Java: Mockitoでハマった落とし穴5つとその解決方法

[Mockito](https://site.mockito.org/)は、Javaプロジェクトのテストコードを書くときに利用できる優れたライブラリです。

しかし、コードの書き方を間違えると期待通りには動作せず、原因究明に時間を浪費してしまうこともあります。
このため、この記事では、私が過去にハマった落とし穴をまとめました。

以下、執筆時点の最新リリースである、Mockito 4.0.0をベースにしています。

# 1. [InvalidUseOfMatchersException](https://javadoc.io/static/org.mockito/mockito-core/4.0.0/org/mockito/exceptions/misusing/InvalidUseOfMatchersException.html)の発生

テストを実行した際に、下記のようなエラーが出力されることがあります。

```
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](https://javadoc.io/doc/org.mockito/mockito-core/latest/org/mockito/ArgumentMatchers.html)とそれ以外の生の値を併用することはできません。

エラーメッセージの例では、第一引数に[ArgumentMatchers#any](https://javadoc.io/doc/org.mockito/mockito-core/latest/org/mockito/ArgumentMatchers.html#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](https://javadoc.io/doc/org.mockito/mockito-core/latest/org/mockito/ArgumentMatchers.html#eq-T-)を指定することで解決しています。

Mockitoは実行時のエラーメッセージに解決方法が書かれていることがありますので、ちゃんとメッセージを読めば解決できることも多いです。


# 2. org.hamcrest.Matchers#anyとの混同によるコンパイルエラー

下記のようなコードを書いたときに、なぜか `any()` の箇所でコンパイルエラーが発生することがあります。

```java
when(foo.getBar(any())).thenReturn("baz");
```

## 原因

多くの場合、これはMockitoで利用する[ArgumentMatchers#any](https://javadoc.io/doc/org.mockito/mockito-core/latest/org/mockito/ArgumentMatchers.html#any--)を利用すべき箇所において、誤ってhamcrestの[Matchers#any](http://hamcrest.org/JavaHamcrest/javadoc/2.2/org/hamcrest/Matchers.html#any-java.lang.Class-)を渡してしまっていることが原因です。おそらく、下記のようなimport文が存在しているのではないでしょうか。

```java
import static org.hamcrest.Matchers.any;
// または
import static org.hamcrest.CoreMatchers.any;
```

Mockitoとhamcrestのstaticメソッドは、いずれも[staticインポート](https://qiita.com/flyaway/items/2356672dd907476fbc1f#3-%E4%B8%AD%E7%B4%9A%E8%80%85%E5%90%91%E3%81%91static%E3%82%A4%E3%83%B3%E3%83%9D%E3%83%BC%E3%83%88)して使われることが多いため、なかなか気付くことができません。

## 解決策

クラスで宣言されているimport static文の記述を確認しましょう。具体的には、下記の記述が存在するかどうかを確認します。もし、存在しなければ追加します。

```java
import static org.mockito.ArgumentMatchers.any;
```

[MockitoクラスはArgumentMatchersクラスを継承している](https://javadoc.io/doc/org.mockito/mockito-core/latest/org/mockito/Mockito.html#any--)ため、下記のimport文を追加することでも動作します。この一行を書いておけば、利用しているIDEの設定によっては、保存時に自動的にimport文を展開してくれます。

```java
import static org.mockito.Mockito.*;
```

または、staticインポートを利用せずに、 `import org.mockito.Mockito;` とした上で、 `Mockito.any` と記述しても構いません。

# 3. [UnnecessaryStubbingException](https://javadoc.io/static/org.mockito/mockito-core/4.0.0/org/mockito/exceptions/misusing/UnnecessaryStubbingException.html)の発生

テストを実行した際に、下記のようなエラーが出力されることがあります。

```
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にも書かれています。

http://blog.mockito.org/2018/07/new-mockito-api-lenient.html


## 解決策

下記の記事が詳しいです。

https://qiita.com/kazuki43zoo/items/8762927ed182878eb58c

複数のテストケースから参照されるべき共通的なMockクラスでは、lenientの指定なしではエラーが避けられないことがあります。このため、そのようなMock処理においては、lenientの利用を検討すると良いでしょう。

# 4. SpyしたクラスのメソッドのMockがうまくいかない

たとえば、下記のように[spy](https://javadoc.io/doc/org.mockito/mockito-core/latest/org/mockito/Spy.html)したクラスに対してMockを試みても、期待しているようにMockされません。

```java
Foo foo = spy(Foo.class);
when(foo.bar()).thenReturn("baz");
```

## 原因

`when(foo.bar())` の時点でbarメソッドが実行されてしまっています。
このため、Spyしたクラスでは`when/thenReturn`構文を利用することはできません。

## 解決策

代わりに`doReturn/when`を利用しましょう。

```java
doReturn("baz").when(foo).bar();
```

stackoverflowにも同様の回答があります。

https://stackoverflow.com/a/20360269

このような落とし穴があるので、この解説では、常にdoReturn/whenを利用することを説いています。
（しかし、私はそのプロジェクトにおける慣例に従うのが良いと思います）

# 5. 引数が存在し、返却値がvoidのstaticメソッド1つだけをMockできない

[mockito-inlineを利用することで、staticメソッドをMockすることができます。](https://javadoc.io/doc/org.mockito/mockito-core/latest/org/mockito/Mockito.html#mockito-inline)

```java
@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;
    }
}
```
[https://github.com/mockito/mockito/blob/v4.0.0/subprojects/inline/src/test/java/org/mockitoinline/StaticMockTest.java#L169-L191](https://github.com/mockito/mockito/blob/v4.0.0/subprojects/inline/src/test/java/org/mockitoinline/StaticMockTest.java#L169-L191)

ただし、引数が存在する特定のstaticメソッド1つをMockすることができません。

## 原因

Mockito 4.0.0時点では、それを実現するAPIは存在していません。

https://github.com/mockito/mockito/issues/2027#issuecomment-684781845

## 解決策

Proxyクラスや[ラップメソッド](https://qiita.com/flyaway/items/34069ca6ad9ada4c0fef)を作成し、それ経由でMockするのがよいでしょう。

```java
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を使って単体テストを書いている方の助けになりますように。