18
14

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

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

Last updated at Posted at 2021-10-30

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;
    }
}

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は存在していません。

解決策

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

18
14
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
18
14

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?