Mockitoは、Javaプロジェクトのテストコードを書くときに利用できる優れたライブラリです。
しかし、コードの書き方を間違えると期待通りには動作せず、原因究明に時間を浪費してしまうこともあります。
このため、この記事では、私が過去にハマった落とし穴をまとめました。
以下、執筆時点の最新リリースである、Mockito 4.0.0をベースにしています。
1. InvalidUseOfMatchersExceptionの発生
テストを実行した際に、下記のようなエラーが出力されることがあります。
org.mockito.exceptions.misusing.InvalidUseOfMatchersException:
Invalid use of argument matchers!
X matchers expected, Y recorded:
-> at ...
原因
実行時に出力されるエラーメッセージの通りです。
This exception may occur if matchers are combined with raw values:
//incorrect:
someMethod(any(), "raw String");
mock対象のメソッドの引数が複数の場合、Matchersとそれ以外の生の値を併用することはできません。
エラーメッセージの例では、第一引数にArgumentMatchers#anyを利用しているのにもかかわらず、第二引数にはString型を指定してしまっています。
解決策
エラーメッセージに書かれている内容に従います。
When using matchers, all arguments have to be provided by matchers.
For example:
//correct:
someMethod(any(), eq("String by matcher"));
For more info see javadoc for Matchers class.
エラーメッセージの例では、第二引数にArgumentMatchers#eqを指定することで解決しています。
Mockitoは実行時のエラーメッセージに解決方法が書かれていることがありますので、ちゃんとメッセージを読めば解決できることも多いです。
2. org.hamcrest.Matchers#anyとの混同によるコンパイルエラー
下記のようなコードを書いたときに、なぜか any()
の箇所でコンパイルエラーが発生することがあります。
when(foo.getBar(any())).thenReturn("baz");
原因
多くの場合、これはMockitoで利用するArgumentMatchers#anyを利用すべき箇所において、誤ってhamcrestのMatchers#anyを渡してしまっていることが原因です。おそらく、下記のようなimport文が存在しているのではないでしょうか。
import static org.hamcrest.Matchers.any;
// または
import static org.hamcrest.CoreMatchers.any;
Mockitoとhamcrestのstaticメソッドは、いずれもstaticインポートして使われることが多いため、なかなか気付くことができません。
解決策
クラスで宣言されているimport static文の記述を確認しましょう。具体的には、下記の記述が存在するかどうかを確認します。もし、存在しなければ追加します。
import static org.mockito.ArgumentMatchers.any;
MockitoクラスはArgumentMatchersクラスを継承しているため、下記のimport文を追加することでも動作します。この一行を書いておけば、利用しているIDEの設定によっては、保存時に自動的にimport文を展開してくれます。
import static org.mockito.Mockito.*;
または、staticインポートを利用せずに、 import org.mockito.Mockito;
とした上で、 Mockito.any
と記述しても構いません。
3. UnnecessaryStubbingExceptionの発生
テストを実行した際に、下記のようなエラーが出力されることがあります。
org.mockito.exceptions.misusing.UnnecessaryStubbingException:
Unnecessary stubbings detected.
Clean & maintainable test code requires zero unnecessary code.
Following stubbings are unnecessary (click to navigate to relevant line of code):
Mockクラスが他のテストケースからも参照されていると、現在編集中のテストケース以外で発生することもあります。
原因
Mockしたメソッドが、そのテストケースの中で呼ばれていない場合に発生します。
これは、不必要なMockを検知して、テストのメンテナンス性を保つことが目的です。
これはMockito 3.0(?)以降の動作で、MockitoのBlogにも書かれています。
解決策
下記の記事が詳しいです。
複数のテストケースから参照されるべき共通的なMockクラスでは、lenientの指定なしではエラーが避けられないことがあります。このため、そのようなMock処理においては、lenientの利用を検討すると良いでしょう。
4. SpyしたクラスのメソッドのMockがうまくいかない
たとえば、下記のようにspyしたクラスに対してMockを試みても、期待しているようにMockされません。
Foo foo = spy(Foo.class);
when(foo.bar()).thenReturn("baz");
原因
when(foo.bar())
の時点でbarメソッドが実行されてしまっています。
このため、Spyしたクラスではwhen/thenReturn
構文を利用することはできません。
解決策
代わりにdoReturn/when
を利用しましょう。
doReturn("baz").when(foo).bar();
stackoverflowにも同様の回答があります。
このような落とし穴があるので、この解説では、常にdoReturn/whenを利用することを説いています。
(しかし、私はそのプロジェクトにおける慣例に従うのが良いと思います)
5. 引数が存在し、返却値がvoidのstaticメソッド1つだけをMockできない
mockito-inlineを利用することで、staticメソッドをMockすることができます。
@Test
public void testStaticMockVoid() {
try (MockedStatic<Dummy> dummy = Mockito.mockStatic(Dummy.class)) {
Dummy.fooVoid("bar");
assertNull(Dummy.var1);
dummy.verify(()->Dummy.fooVoid("bar"));
}
Dummy.fooVoid("bar");
assertEquals("bar", Dummy.var1);
}
static class Dummy {
static String var1 = null;
static String foo() {
return "foo";
}
static void fooVoid(String var2) {
var1 = var2;
}
}
ただし、引数が存在する特定のstaticメソッド1つをMockすることができません。
原因
Mockito 4.0.0時点では、それを実現するAPIは存在していません。
解決策
Proxyクラスやラップメソッドを作成し、それ経由でMockするのがよいでしょう。
class DummyProxy {
void fooVoid(String var2) {
Dummy.fooVoid(var2);
}
}
@Test
public void test() {
DummyProxy dummy = mock(DummyProxy.class);
doAnswer(answer()).when(dummy).fooVoid("bar");
}
Answer answer() {
return ((invocation) -> {
String var2 = invocation.getArgument(0, String.class);
// do something
return null;
});
}
おわりに
他にも何か落とし穴を見つけましたら、ぜひコメントで紹介していただけると嬉しいです。
この記事が、Mockitoを使って単体テストを書いている方の助けになりますように。