1
1

More than 3 years have passed since last update.

[補足] org.mockito.exceptions.misusing.UnnecessaryStubbingExceptionについて

Last updated at Posted at 2021-06-23

背景

以下記事は [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も作り方は基本的に一緒 (<=ここが自分にとってかなりややこしかった :disappointed:)。Mockの場合は verifyをコールして動作の検証を行う一方、Stubの場合はそれを行わない(実装が壊れないようにフェイクデータを返すことが責務だから)。

参考LINK
まとめ
  • 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 モードを利用することは必ずしも悪いことではないと言っている (エラーはデフォルトで出しておきながら、、 :sweat_drops: )

理由としては、、

  • 上記のように、@Beforeメソッド時に、繰り返し的にStubの共通挙動を定義することで、コードの冗長化が減る

    • なので、上記例 (test3) のように、一部やむを得ない例外事項が発生してしまったとしても、重複コードの冗長化 (=>@Beforeメソッドの代わりに、各テストメソッドに同じ処理を重複して書き込む)よりは、 lenient モードを使い、(stubの冗長化をトレードオフし)@Beforeメソッドで初期化処理をまとめた方が綺麗である
  • ただ、いずれにしてもケースバイケースなので、開発者自身の best judgement に委ねる

私の会社のプロジェクトの場合は、、、 :thinking:

まさに lenient 利用のススメ と同じようなトレードオフ現象に遭遇したわけだが、私のテストの場合は、とりあえず@Beforeメソッドから共通処理を外し、各テストメソッドごとに特有の初期化処理を実装するようにした。

どうしてそのような判断をしたかというと、、、

  • テストケースごとに、このオブジェクトはStub初期化処理して、これはしない、というケースが複雑だったから

    • 場合によっては、テストケース時に、Before メソッドで定義したStubを resetする必要があった
    • ちなみに、resetを使うこともmockitoのベストプラクティスとしてはお勧めされていない
    • このままの状態で letientモードを使い共通化を優先してしまうと、後ほど別の開発者(いや私ですら)がテストコードを修正したり、デバッグしたり、となった時、非常に煩雑で読みにくい実装になるだろうと懸念されるから
  • また、各Stub初期化処理をメソッドにまとめる (例: setUpXXXServiceStub みたいにdescriptiveなメソッド名で)と、別に各テストコード内に記載しても、それなりに読みやすくキープできる

    • 同時に、各テストケース/テスト意図ごとに、オブジェクトがどのようにスタブされるべきであるかが、クリアにわかり良い判断だとは思う
1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1