1. ホーム
  2. java

[解決済み] Mockitoのマッチャーはどのように機能するのですか?

2022-06-28 14:36:26

質問

Mockitoの引数マッチャー(例えば any , argThat , eq , same そして ArgumentCaptor.capture() など)はHamcrest matcherとは全く異なる振る舞いをします。

  • Mockitoのマッチャーは、マッチャーが使用されたずっと後に実行されるコードであっても、頻繁にInvalidUseOfMatchersExceptionを引き起こします。

  • Mockitoのマッチャーは、与えられたメソッドの1つの引数がマッチャーを使用する場合、すべての引数に対してMockitoのマッチャーの使用のみを要求するなど、奇妙なルールに従順である。

  • をオーバーライドすると、Mockito matcherがNullPointerExceptionを引き起こす可能性があります。 Answer をオーバーライドした場合や (Integer) any() などがあります。

  • Mockitoのマッチャーを使ったコードを特定の方法でリファクタリングすると、例外や予期せぬ動作が発生したり、完全に失敗したりすることがあります。

Mockitoのマッチャーはなぜこのような設計になっているのか、またどのように実装されているのか?

どのように解決するのですか?

Mockito マッチャー は静的メソッドとそのメソッドの呼び出しで、これは 引数の代わりとなる の呼び出しの際に whenverify .

ハムクレストマッチャー (アーカイブ版) (あるいは Hamcrest-style matchers) は,ステートレスで汎用的なオブジェクトインスタンスです. Matcher<T> を実装し、メソッド matches(T) を公開し、オブジェクトが Matcher の条件にマッチする場合に true を返すメソッドを提供します。これらは副作用がないことを意図しており、一般的に以下のようなアサーションで使用されます。

/* Mockito */  verify(foo).setPowerLevel(gt(9000));
/* Hamcrest */ assertThat(foo.getPowerLevel(), is(greaterThan(9000)));

Hamcrestスタイルのマッチャーとは別に、Mockitoマッチャーが存在します。 マッチング式の記述が直接メソッド呼び出しにフィットするように : Mockitoのマッチャーが返す T ここで Hamcrest matcher のメソッドは Matcher オブジェクト ( Matcher<T> ).

Mockitoのマッチャーは、以下のような静的メソッドで呼び出されます。 eq , any , gt そして startsWith について org.mockito.Matchersorg.mockito.AdditionalMatchers . また、Mockitoのバージョンによって変更されたアダプタもあります。

  • Mockito 1.xの場合。 Matchers は一部の呼び出しを特徴としています (例えば intThat または argThat ) は、Hamcrest matcher を直接パラメータとして受け付ける Mockito matcher です。 ArgumentMatcher<T> 拡張 org.hamcrest.Matcher<T> は、ハムクレストの内部表現で使用されていたものであり Hamcrest matcher の基底クラス であり、Mockito matcherのようなものではありません。
  • Mockito 2.0+では、MockitoはHamcrestに直接依存しなくなりました。 Matchers の呼び出しは、以下のように表現されます。 intThat または argThat ラップ ArgumentMatcher<T> を実装していないオブジェクトは org.hamcrest.Matcher<T> を実装していませんが、似たような方法で使用されています。のような Hamcrest アダプタは argThatintThat はまだ利用可能ですが、移動して MockitoHamcrest に移動しました。

マッチャーがHamcrestであるか、単にHamcrestスタイルであるかにかかわらず、このように適応させることができます。

/* Mockito matcher intThat adapting Hamcrest-style matcher is(greaterThan(...)) */
verify(foo).setPowerLevel(intThat(is(greaterThan(9000))));

上記の文の中に foo.setPowerLevel はメソッドで int . is(greaterThan(9000))Matcher<Integer> として動作しません。 setPowerLevel 引数として機能しません。Mockitoのマッチャーである intThat はそのHamcrestスタイルのMatcherをラップし、その上に int を返すので は引数として現れます。 gt(9000) のようなMockitoのマッチャーは、例のコードの最初の行のように、その式全体を1つの呼び出しにラップします。

マッチャーが行うこと/返すこと

when(foo.quux(3, 5)).thenReturn(true);

引数マッチャーを使用しない場合、Mockitoは引数の値を記録し、その値と equals メソッドと比較します。

when(foo.quux(eq(3), eq(5))).thenReturn(true);    // same as above
when(foo.quux(anyInt(), gt(5))).thenReturn(true); // this one's different

のようなマッチャを呼び出すと any あるいは gt (より大きい)の場合、Mockitoはマッチャー・オブジェクトを保存し、そのマッチャー・オブジェクトはMockitoにその等質性チェックをスキップさせ、選択したマッチを適用させます。の場合 argumentCaptor.capture() の場合、後で検査するために代わりにその引数を保存するマッチャーを格納します。

マッチャーは ダミー値 ゼロや空のコレクション、あるいは null . Mockitoは安全で適切なダミー値を返そうとします. anyInt() または any(Integer.class) または空の List<String> に対して anyListOf(String.class) . しかし,型消去のため,Mockitoは型情報を欠き null に対して any() または argThat(...) を自動アンボックスしようとすると、NullPointerException が発生することがあります。 null プリミティブな値です。

のようなマッチャーは eqgt はパラメータを受け取ります。理想的には、これらの値はスタブや検証を開始する前に計算されるべきです。他の呼び出しをモックしている最中にモックを呼び出すと、 スタブ動作に支障をきたす可能性があります。

Matcherのメソッドは戻り値として使用することはできません。 thenReturn(anyInt()) または thenReturn(any(Foo.class)) といったように、Mockitoでは Mockitoはスタブ呼び出しの際にどのインスタンスを返すかを正確に知る必要があり、任意の戻り値を選んでくれるわけではありません。

実装の詳細

マッチャーは、(Hamcrestスタイルのオブジェクトマッチャーとして) 引数マッチャー格納 . MockitoCoreとMatchersはそれぞれ スレッドセーフモッキングプログレス インスタンスを所有し、その 静的に は MockingProgress インスタンスを保持する ThreadLocal を含んでいます。これは MockingProgressImpl であり、具体的な ArgumentMatcherStorageImpl . その結果、モックとマッチャーの状態は静的ですが、MockitoクラスとMatchersクラスの間で一貫してスレッドスコープされます。

ほとんどの matcher の呼び出しはこのスタックに追加されるだけですが、例外として、以下のような matcher があります。 and , or そして not . これは完全に の評価順序に完全に対応しています。 に完全に対応しており、メソッドを呼び出す前に引数を左から右に評価します。

when(foo.quux(anyInt(), and(gt(10), lt(20)))).thenReturn(true);
[6]      [5]  [1]       [4] [2]     [3]

これで

  1. 追加 anyInt() をスタックに追加します。
  2. 追加する gt(10) をスタックに追加します。
  3. 追加する lt(20) をスタックに追加します。
  4. 削除する gt(10)lt(20) を追加し and(gt(10), lt(20)) .
  5. コール foo.quux(0, 0) を呼び出すと、(特にスタブがない限り)デフォルトの値である false . 内部的には,Mockitoは quux(int, int) を最も新しい呼び出しとしてマークします。
  6. 呼び出し when(false) を呼び出すと、引数を破棄してスタブメソッド quux(int, int) 5で識別される。有効な状態はスタック長0(等号)または2(マッチャー)のみであり,スタック上に2つのマッチャーがあるので(ステップ1と4),モッキートはメソッドを any() マッチャーを第1引数に、そして and(gt(10), lt(20)) を返し、スタックをクリアします。

これはいくつかのルールを示しています。

  • Mockito は quux(anyInt(), 0)quux(0, anyInt()) . どちらも quux(0, 0) の呼び出しのように見えますが、スタック上に1つの int matcher があります。その結果、もし1つのマッチャーを使うなら、すべての引数にマッチしなければなりません。

  • 呼び出し順は単に重要なだけでなく このすべてを機能させるものです。 . 変数へのマッチャーの抽出は、通常、呼び出し順を変更するため、うまくいきません。しかし、メソッドへのマッチャーの抽出は非常にうまくいきます。

    int between10And20 = and(gt(10), lt(20));
    /* BAD */ when(foo.quux(anyInt(), between10And20)).thenReturn(true);
    // Mockito sees the stack as the opposite: and(gt(10), lt(20)), anyInt().
    
    public static int anyIntBetween10And20() { return and(gt(10), lt(20)); }
    /* OK */  when(foo.quux(anyInt(), anyIntBetween10And20())).thenReturn(true);
    // The helper method calls the matcher methods in the right order.
    
    
  • スタックは頻繁に変化するので、Mockitoはあまり注意深くそれを取り締まることができません。Mockitoやモックと対話するときにしかスタックをチェックできませんし、マッチャーがすぐに使われるか、偶然に放棄されるかを知らずにマッチャーを受け入れざるを得ません。理論的には、スタックは常に when または verify などがありますが、Mockitoは自動でチェックできません。 手動でチェックするには Mockito.validateMockitoUsage() .

  • の呼び出しで when を呼び出すと、Mockitoは実際に問題のメソッドを呼び出します。例外を投げるようにメソッドをスタブ化した場合(または非ゼロまたは非ヌル値を要求した場合)、例外がスローされます。 doReturndoAnswer (など)する ではない は実際のメソッドを呼び出さないので、しばしば有用な代替手段となります。

  • スタブしている途中でモックメソッドを呼び出していた場合 (例えば eq マッチャーの答えを計算するため)、Mockito はスタックの長さを <項目 その を呼び出すことになり、失敗する可能性が高いです。

  • もしあなたが何か悪いことをしようとしたら、例えば 最終的なメソッドをスタブする/検証する のような悪いことをしようとすると、 Mockitoは本当のメソッド を呼び出し、さらにスタック上に余分なマッチャーを残します。 . そのため final メソッド呼び出しは例外を投げないかもしれませんが、その場合 InvalidUseOfMatchersExceptionが発生する可能性があります。 が発生する可能性があります。

よくある問題

  • 無効なマッチャの使用例外 :

    • もし matcher を使うなら、すべての引数が正確に1つの matcher 呼び出しを持っていること、そして when または verify を呼び出します。マッチャーは決してスタブの戻り値やフィールド/変数として使用してはいけません。

    • マッチャーの引数を提供する一部としてモックを呼び出していないことを確認してください。

    • matcherでfinalメソッドのスタブやベリファイをしようとしていないか確認してください。スタック上にマッチャーを残すのは素晴らしい方法で、finalメソッドが例外を投げない限り、モックしているメソッドがfinalであることに気づくのはこの時だけかもしれません。

  • プリミティブな引数でNullPointerExceptionが発生しました。 (Integer) any() はnullを返しますが any(Integer.class) は0を返します。 NullPointerException を期待している場合は int を期待しているのであれば、それは整数ではありません。いずれにせよ anyInt() これはゼロを返し、オートボックスのステップもスキップします。

  • NullPointerExceptionなどの例外が発生します。 への呼び出し when(foo.bar(any())).thenReturn(baz) を呼び出すと、実際には を呼び出します。 foo.bar(null) で、NULL引数を受け取ったときに例外を投げるようにスタブしていたかもしれません。に切り替えると doReturn(baz).when(foo).bar(any()) に切り替えると、スタブ化した振る舞いをスキップします。 .

一般的なトラブルシューティング

  • 使用方法 MockitoJUnitRunnerを使用する。 を使うか、明示的に validateMockitoUsage の中で tearDown または @After メソッド(これはランナーが自動的にやってくれるでしょう)を使用します。これは、マッチャーを誤って使用したかどうかを判断するのに役立ちます。

  • デバッグのために validateMockitoUsage を直接コードに追加してください。これはスタック上に何かあれば投げるので、悪い症状の良い警告となります。