背景
以下記事は [Web/まとめ] JUnit5 で Mockitoを利用する方法で理解に若干時間のかかった org.mockito.exceptions.misusing.UnnecessaryStubbingException
について、実際のサンプルコードを踏まえて、理解の足しにした追加メモである。
###一体どのような時に起こるのか?
(復習&まとめ)まずMockitoで何ができるのか?
- Mockitoを利用することで、テスト対象コードが利用する依存サービス (3rd party libraryだったり、実際の実装がわからない interface classだったり)への依存なしでテストを行うことができる。
- 例えば、実際の実装を使わずとも、Mockオブジェクトを定義し、そのMock オブジェクトがどのような結果を返すかなどを、開発者は各テストケースに応じて挙動を書き換える (=定義する)ことができる
- 利用している3rd library interfaceがExceptionをthrowした場合は、自分のテストクラスはどのような挙動をするべきか?
- 3rd party library interface / method の実装や中身は興味がない、とにかくintegrationが正常に行われていると仮定して、3rd party library interface の挙動をフェイク実装したい
(復習&まとめ) TestにおけるStubとMockとは?
自分は、Mockitoにおいて、ここやや混同したのでまとめておく。
MockとStubの違い
**Mockitoの場合は StubもMockも作り方は基本的に一緒 (<=ここが自分にとってかなりややこしかった )。Mockの場合は `verify`をコールして動作の検証を行う一方、Stubの場合はそれを行わない(実装が壊れないようにフェイクデータを返すことが責務だから)。**参考LINK
- https://stackoverflow.com/questions/463707/what-are-the-differences-between-mocks-and-stubs-on-rhino-mocks
- https://pazoo.hatenablog.com/entry/stub_mock_Java (わかりやすい説明 )
- https://javadoc.io/static/org.mockito/mockito-core/3.11.2/org/mockito/Mockito.html#verification (Mockito公式Doc/Mockについて)
- https://javadoc.io/static/org.mockito/mockito-core/3.11.2/org/mockito/Mockito.html#stubbing (Mockito公式Doc/Stubについて)
まとめ
-
Mock
- テスト対象のクラスに対するテストの一環として、(実装は持たない架空の)interface/componentの 予測される定義を記述し、実際に期待通りコールされたかをチェックする
- 従って、Mockitoの場合は
verify
を呼ぶことが必須!!! - ただ、それ以外の作成方法は一緒! (例:
@Mock
を利用)
-
Stub
- テストコードがテストケースの意図と異なる箇所で関連のないエラーが発生しないよう、問題ないFakeデータを返すように定義するためだけに、挙動を定義する
- 従って、Mockitoの場合は
verify
はむしろ実行してはいけない! (Mockito java docではコードが冗長になるのでやめろと言っている - (参考) https://javadoc.io/static/org.mockito/mockito-core/3.11.2/org/mockito/Mockito.html#stubbing
- ただ、それ以外の作成方法は一緒! (例:
@Mock
を利用)
(話題の核心) UnnecessaryStubbingExceptionはどのような問題で発生するのか?
ここまで前提知識が詰まったところでようやく核心に入れる。。
このExceptionは、Mock
についてではなく Stub
について述べている。
上記で述べたとおり、テストコードを無事テスト意図通りに動かすためだけのFake Dataを返せば良いというタイプのオブジェクトであるが、注意しなければならないのが、、
やみくもに Stub Codeをテスト対象コード、テスト意図をきちんと理解しないまま、テストを動かすだけに適当につけていってしまうと、テストコードが読みにくくなり、テストが汚くなってしまうということなのだ!
この問題に対してアラートを検知してくれる仕組みがこのExceprtionなのだ!
実際には、Mocktito JavaDocでわかりやすい例を表示してくれていた (https://javadoc.io/static/org.mockito/mockito-core/3.11.2/org/mockito/Mockito.html)
例:
以下テストでは before()
メソッドにより、毎テストケース実行時に when(foo.foo()).thenReturn("ok");
が定義されている。
これは、test1 & test2 では問題なく動作するが、 test3では foo
に対する interactionが発生していないため、when(foo.foo()).thenReturn("ok");
というスタブ定義が冗長になってしまい、 UnnecessaryStubbingException
が発生してしまう。
public class SomeTest {
@Rule public MockitoRule mockito = MockitoJUnit.rule().strictness(STRICT_STUBS);
@Mock Foo foo;
@Mock Bar bar;
@Before public void before() {
when(foo.foo()).thenReturn("ok");
// it is better to configure the stubbing to be lenient:
// lenient().when(foo.foo()).thenReturn("ok");
// or the entire mock to be lenient:
// foo = mock(Foo.class, withSettings().lenient());
}
@Test public void test1() {
foo.foo();
}
@Test public void test2() {
foo.foo();
}
@Test public void test3() {
bar.bar();
}
}
lenient
利用のススメ
このtest errorは mock objectに対して lenient
モードを利用することによって消去することができる。
上記 公式Mockito documentationでは、別にそうして lenient
モードを利用することは必ずしも悪いことではないと言っている (エラーはデフォルトで出しておきながら、、 )
理由としては、、
-
上記のように、
@Before
メソッド時に、繰り返し的にStubの共通挙動を定義することで、コードの冗長化が減る- なので、上記例 (test3) のように、一部やむを得ない例外事項が発生してしまったとしても、重複コードの冗長化 (=>
@Before
メソッドの代わりに、各テストメソッドに同じ処理を重複して書き込む)よりは、lenient
モードを使い、(stubの冗長化をトレードオフし)@Before
メソッドで初期化処理をまとめた方が綺麗である
- なので、上記例 (test3) のように、一部やむを得ない例外事項が発生してしまったとしても、重複コードの冗長化 (=>
-
ただ、いずれにしてもケースバイケースなので、開発者自身の best judgement に委ねる
私の会社のプロジェクトの場合は、、、
まさに lenient
利用のススメ と同じようなトレードオフ現象に遭遇したわけだが、私のテストの場合は、とりあえず@Before
メソッドから共通処理を外し、各テストメソッドごとに特有の初期化処理を実装するようにした。
どうしてそのような判断をしたかというと、、、
-
テストケースごとに、このオブジェクトはStub初期化処理して、これはしない、というケースが複雑だったから
- 場合によっては、テストケース時に、
Before
メソッドで定義したStubを resetする必要があった -
ちなみに、
reset
を使うこともmockitoのベストプラクティスとしてはお勧めされていない - このままの状態で
letient
モードを使い共通化を優先してしまうと、後ほど別の開発者(いや私ですら)がテストコードを修正したり、デバッグしたり、となった時、非常に煩雑で読みにくい実装になるだろうと懸念されるから
- 場合によっては、テストケース時に、
-
また、各Stub初期化処理をメソッドにまとめる (例:
setUpXXXServiceStub
みたいにdescriptiveなメソッド名で)と、別に各テストコード内に記載しても、それなりに読みやすくキープできる- 同時に、各テストケース/テスト意図ごとに、オブジェクトがどのようにスタブされるべきであるかが、クリアにわかり良い判断だとは思う