LoginSignup
54
48

More than 1 year has passed since last update.

Mockito使い方メモ

Last updated at Posted at 2022-07-18

Mockito とは

  • Java のテストでモックを簡単に扱えるようにするためのフレームワーク
  • 指定されたクラスのモックを作成し、任意のメソッドをスタブ化して指定した値を返すようにしたり、モックのメソッドが期待した通りに呼び出されたかどうかを検証したりできる
  • staticfinal なメソッドのモック化もできたりする(昔はできなかったはずだけど、いつの頃からかできるようになってた)
  • モックの振る舞いの定義を型安全に定義できるのが大きな特徴(だと思う)
  • 読み方は「もきーと」

環境

> gradle --version

------------------------------------------------------------
Gradle 7.4.2
------------------------------------------------------------

Build time:   2022-03-31 15:25:29 UTC
Revision:     540473b8118064efcc264694cbcaa4b677f61041

Kotlin:       1.5.31
Groovy:       3.0.9
Ant:          Apache Ant(TM) version 1.10.11 compiled on July 10 2021
JVM:          18.0.1.1 (Oracle Corporation 18.0.1.1+2-6)
OS:           Windows 10 10.0 amd64

Hello World

実装

build.gradle
plugins {
    id "java"
}

sourceCompatibility = 18
targetCompatibility = 18

[compileJava, compileTestJava]*.options*.encoding = "UTF-8"

repositories {
    mavenCentral()
}

dependencies {
    testImplementation "org.mockito:mockito-core:4.6.1"
    testImplementation "org.junit.jupiter:junit-jupiter:5.8.2"
}

test {
    useJUnitPlatform()
}
package sandbox.mockito;

import org.junit.jupiter.api.Test;

import java.util.List;

import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;

public class HelloMockitoTest {

    @Test
    void test() {
        List<String> mock = mock(List.class);

        when(mock.get(0)).thenReturn("Hello Mockito!");

        System.out.println(mock.get(0));
    }
}
実行結果
Hello Mockito!

説明

build.gradle
dependencies {
    testImplementation "org.mockito:mockito-core:4.6.1"
    ...
}
        List<String> mock = mock(List.class);
  • Mockito#mock(Class<?>) で、指定した型のモックオブジェクトを生成する
  • ここでは、 java.util.List のモックオブジェクトを生成している
        when(mock.get(0)).thenReturn("Hello Mockito!");
  • Mockito#when(T) などの API を使い、モックの振る舞いを定義する
  • ここでは、モックの get(0) メソッドを実行したら、 "Hello Mockito!" という文字列を返すように定義している
    • when() メソッドの引数で、スタブ化したいメソッドの振る舞いを実行するように実装する
    • thenReturn(T) で、 when() メソッドの引数で実行したメソッドが返す値を指定する
    • when(T) が受け取る型と thenReturn(T) が受け取る型は一致するようになっているので、間違った型を指定した場合はコンパイルエラーになる(型安全)

モックのデフォルトの振る舞い

package sandbox.mockito;

import org.junit.jupiter.api.Test;

import java.util.List;

import static org.mockito.Mockito.mock;

public class DefaultReturnValueTest {
    @Test
    void test() {
        Hoge mock = mock(Hoge.class);

        System.out.println("mock.anyObject()        = " + mock.anyObject());
        System.out.println("mock.primitive()        = " + mock.primitive());
        System.out.println("mock.primitiveWrapper() = " + mock.primitiveWrapper());
        System.out.println("mock.array()            = " + mock.array());
        System.out.println("mock.collection()       = " + mock.collection());
    }

    public class Hoge {
        public String anyObject() {
            System.out.println("Hello World");
            return "Hello World";
        }

        public int primitive() {
            return 1;
        }

        public Integer primitiveWrapper() {
            return 2;
        }

        public int[] array() {
            return new int[] {1, 2, 3};
        }

        public List<String> collection() {
            return List.of("hello", "world");
        }
    }
}
実行結果
mock.anyObject()        = null
mock.primitive()        = 0
mock.primitiveWrapper() = 0
mock.array()            = null
mock.collection()       = []
  • モックのメソッドは、本来の処理を一切行わなくなる
  • モックのメソッドは、以下のように値を返すようになる
    • プリミティブ型・ラッパー型
      • デフォルト値
      • int なら 0, boolean なら false など
    • コレクション型
      • 空のコレクション
    • 配列・その他オブジェクト
      • null

スタブを定義する

package sandbox.mockito;

import org.junit.jupiter.api.Test;

import java.util.List;

import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;

public class WhenTest {
    @Test
    void test() {
        List<String> mock = mock(List.class);

        when(mock.get(0)).thenReturn("hello");
        when(mock.get(1)).thenReturn("world");

        System.out.println("mock.get(0) = " + mock.get(0));
        System.out.println("mock.get(0) = " + mock.get(0));
        System.out.println("mock.get(0) = " + mock.get(0));
        System.out.println("mock.get(1) = " + mock.get(1));
        System.out.println("mock.get(2) = " + mock.get(2));

        when(mock.get(1)).thenReturn("Override");
        System.out.println("mock.get(1) = " + mock.get(1));
    }
}
実行結果
mock.get(0) = hello
mock.get(0) = hello
mock.get(0) = hello
mock.get(1) = world
mock.get(2) = null
mock.get(1) = Override
  • when(<スタブ化したいメソッドの呼び出し>).thenReturn(<メソッドが返す値>) とすることで、モックの任意のメソッドが返す値を指定できる
  • 一度スタブ化されたメソッドは、常に同じ値を返すようになる
  • スタブ化は引数も条件に含まれているため、スタブ化したときとは異なる引数でメソッドを実行した場合は指定された値は返されない
  • スタブは上書きが可能で、同じ条件でスタブを再定義すると新しく指定された値を返すようになる

実行するごとに異なる値を返すようにスタブ化する

    @Test
    void testConsecutiveCalls() {
        List<String> mock = mock(List.class);

        when(mock.get(0)).thenReturn("one", "two", "three");

        System.out.println("mock.get(0) = " + mock.get(0));
        System.out.println("mock.get(0) = " + mock.get(0));
        System.out.println("mock.get(0) = " + mock.get(0));
        System.out.println("mock.get(0) = " + mock.get(0));
    }
実行結果
mock.get(0) = one
mock.get(0) = two
mock.get(0) = three
mock.get(0) = three
  • 引数が可変長引数の thenReturn(T...) を使用すれば、実行するごとにメソッドが返す値が変わるようにスタブ化できる
  • 指定された数以上メソッドが実行された場合は、最後に指定した値を返し続ける

例外をスローするようにスタブ化する

    @Test
    void testThrowException() {
        List<String> mock = mock(List.class);

        when(mock.get(0)).thenThrow(new RuntimeException("test"));

        try {
            mock.get(0);
        } catch (Throwable e) {
            System.out.println("e = " + e);
        }
    }
実行結果
e = java.lang.RuntimeException: test
  • thenThrow(Throwable) を使用すると、メソッドを実行したときに例外をスローするようにスタブ化できる

実行するごとに異なる例外をするようにスタブ化する

    @Test
    void testConsecutiveThrowException() {
        List<String> mock = mock(List.class);

        when(mock.get(0)).thenThrow(
            new RuntimeException("one"),
            new NullPointerException("two"),
            new IllegalArgumentException("three")
        );
        try {
            mock.get(0);
        } catch (Throwable e1) {
            System.out.println("e1 = " + e1);
        }
        try {
            mock.get(0);
        } catch (Throwable e2) {
            System.out.println("e2 = " + e2);
        }
        try {
            mock.get(0);
        } catch (Throwable e3) {
            System.out.println("e3 = " + e3);
        }
        try {
            mock.get(0);
        } catch (Throwable e4) {
            System.out.println("e4 = " + e4);
        }
    }
実行結果
e1 = java.lang.RuntimeException: one
e2 = java.lang.NullPointerException: two
e3 = java.lang.IllegalArgumentException: three
e4 = java.lang.IllegalArgumentException: three
  • 引数が可変長引数の thenThrow(Throwable...) を使用すれば、実行するごとにメソッドがスローする例外が変わるようにスタブ化できる
  • 指定された数以上メソッドが実行された場合は、最後に指定した例外をスローし続ける

戻り値と例外のスローを組み合わせてスタブ化する

    @Test
    void testConsecutiveThrowAndReturn() {
        List<String> mock = mock(List.class);

        when(mock.get(0))
            .thenReturn("one")
            .thenThrow(new RuntimeException("two"))
            .thenReturn("three");

        System.out.println("mock.get(0) = " + mock.get(0));
        try {
            mock.get(0);
        } catch (Throwable e) {
            System.out.println("e = " + e);
        }
        System.out.println("mock.get(0) = " + mock.get(0));
    }
実行結果
mock.get(0) = one
e = java.lang.RuntimeException: two
mock.get(0) = three
  • thenReturn() および thenThrow() をメソッドチェーンでつなげることで、戻り値の指定と例外のスローの指定を組み合わせてスタブを定義できる
  • スタブの定義は上書きされるので、チェーンさせずに個別に定義すると最後の定義だけが有効になるので注意
when(mock.get(0)).thenReturn("one"); // 上書きされる
when(mock.get(0)).thenThrow(new RuntimeException("two")); // 上書きされる
when(mock.get(0)).thenReturn("three"); // これだけ有効になる
  • こう定義してしまうと、最後の thenReturn("three") だけが有効になってしまう、ということ

任意の引数に対してスタブ化させる

    @Test
    void testAnyArgumentMatch() {
        List<String> mock = mock(List.class);

        when(mock.get(anyInt())).thenReturn("hoge");

        System.out.println("mock.get(0) = " + mock.get(0));
        System.out.println("mock.get(1) = " + mock.get(1));
        System.out.println("mock.get(2) = " + mock.get(2));
    }
実行結果
mock.get(0) = hoge
mock.get(1) = hoge
mock.get(2) = hoge
  • ArgumentMatchers に定義されている any で始まるメソッド(anyInt(), anyList(), any() など)を引数に使用してスタブを定義すると、任意の引数でのメソッド実行をスタブ化できる

ArgumentMachers に用意されているマッチ用のメソッド

ArgumentMatchers には、よく使いそうなマッチ用のメソッドが用意されている。

anyInt()anyCollection() のような any型名() は、null 以外の 型名 の型に一致する任意の値にマッチする。

以下で、 any型名() 以外のメソッドについて、マッチの条件をまとめる。
eq()~That() は使いどころがちょっと違うので、次の項で説明)

メソッド 説明
startsWith(String) 指定した文字列で始まる
endsWith(String) 指定した文字列で終わる
contains(String) 指定した文字列を含む
matches(String) 指定した正規表現にマッチする
isNull() 引数がnullである
isNotNull(), notNull() 引数がnullでない
any() nullを含む、あらゆる値にマッチする
any(Class<T>), isA(Class<T>) null以外の、指定された型の任意の値にマッチする
nullable(Class<T>) nullを含む、指定された型の任意の値にマッチする
same(T) 指定したオブジェクトと同じオブジェクトかどうか

nullable()any() の違いはよく分からない。

複数ある引数のうち一部だけにマッチングを適用する

    @Test
    void testStubbingMultipleArgumentsMethod_invalidCase() {
        BiFunction<String, String, String> mock = mock(BiFunction.class);

        when(mock.apply("hello", anyString())).thenReturn("mocked");

        System.out.println(mock.apply("hello", "world"));
    }
  • 第二引数だけ任意の文字列にしようとしている
実行結果
Invalid use of argument matchers!
2 matchers expected, 1 recorded:
-> at sandbox.mockito.StubbingTest.testStubbingMultipleArgumentsMethod(StubbingTest.java:200)

This exception may occur if matchers are combined with raw values:
    //incorrect:
    someMethod(any(), "raw 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.

org.mockito.exceptions.misusing.InvalidUseOfMatchersException: 
Invalid use of argument matchers!
2 matchers expected, 1 recorded:
-> at sandbox.mockito.StubbingTest.testStubbingMultipleArgumentsMethod(StubbingTest.java:200)

This exception may occur if matchers are combined with raw values:
    //incorrect:
    someMethod(any(), "raw String");
When using matchers, all arguments have to be provided by matchers.
For example:
    //correct:
    someMethod(any(), eq("String by matcher"));
...
  • エラーになる
  • 引数が複数あるメソッドをスタブ化するときに1つでもマッチングで条件を設定した場合は、他のすべての引数も同じようにマッチングで条件を設定しなければ上記のようなエラーになる
    @Test
    void testStubbingMultipleArgumentsMethod_validCase() {
        BiFunction<String, String, String> mock = mock(BiFunction.class);

        when(mock.apply(eq("hello"), anyString())).thenReturn("mocked");

        System.out.println(mock.apply("hello", "world"));
    }
実行結果
mocked
  • ArgumentMatcherseq() メソッドを使って、第一引数の方もマッチング形式で条件を設定すればエラーは発生しなくなる

自作のマッチング処理を指定する

    @Test
    void testCustomArgumentMatcher() {
        Function<String, String> mock = mock(Function.class);

        ArgumentMatcher<String> isHoge = arg -> arg.equals("hoge");

        when(mock.apply(argThat(isHoge))).thenReturn("HOGE!!!");

        System.out.println("mock.apply(hoge) = " + mock.apply("hoge"));
        System.out.println("mock.apply(fuga) = " + mock.apply("fuga"));
    }
実行結果
mock.apply(hoge) = HOGE!!!
mock.apply(fuga) = null
  • ArgumentMatchersargThat(ArgumentMatcher) を使用すると、マッチング処理を実装した任意の ArgumentMatcher を使って条件を指定できる

スタブ化したメソッド内で任意の処理を実行する

    @Test
    void testAnswer() {
        Function<String, String> mock = mock(Function.class);

        Answer<String> answer = invocation -> {
            System.out.println("arguments = " + Arrays.toString(invocation.getArguments()));
            return "world";
        };

        when(mock.apply("hello")).thenAnswer(answer);

        System.out.println("mock.apply(hello) = " + mock.apply("hello"));
        System.out.println("mock.apply(hey)   = " + mock.apply("hey"));
    }
実行結果
arguments = [hello]
mock.apply(hello) = world
mock.apply(hey)   = null
  • thenAnswer(Answer) を使用すると、スタブ化したメソッドが呼ばれたときに実行する処理を指定できる
  • スタブの処理は Answer で実装する
    • answer(InvocationOnMock) メソッドを実装する
    • 引数で受け取る InvocationOnMock から、実際に渡された引数を取り出すことができる
  • Answer1, Answer2, Answer3 というインタフェースが用意されていて、引数を型安全に受け取りたい場合に利用できる
import static org.mockito.AdditionalAnswers.answer;
...

public class StubbingTest {
    ...
    @Test
    void testAnswer1() {
        Function<String, String> mock = mock(Function.class);

        Answer1<String, String> answer1 = arg -> {
            System.out.println("arg = " + arg);
            return "world";
        };

        when(mock.apply("hello")).thenAnswer(answer(answer1));

        System.out.println("mock.apply(hello) = " + mock.apply("hello"));
        System.out.println("mock.apply(hey)   = " + mock.apply("hey"));
    }
実行結果
arg = hello
mock.apply(hello) = world
mock.apply(hey)   = null
  • AdditionalAnswersanswer()Answer1Answer6 を渡して使用する
  • Answer1 などでは、 InvocationOnMock ではなく渡された引数を直接受け取ることができる

スタブが色々処理をしすぎると、テストの意味が無くなったりメンテナンスがしづらくなる(私見)ので、乱用は避けた方がいい。
公式でも、極力 thenReturn()thenThrow() だけでスタブを定義することが推奨されている。

We recommend simply stubbing with thenReturn() or thenThrow(), which should be enough to test/test-drive any clean and simple code.

11. Stubbing with callbacks

戻り値が void のメソッドをスタブ化(例外をスローさせるように)する

    @Test
    void testReturnVoid() {
        Runnable mock = mock(Runnable.class);

        doThrow(new RuntimeException("test")).when(mock).run();

        try {
            mock.run();
        } catch (Throwable e) {
            System.out.println("e = " + e);
        }
    }
実行結果
e = java.lang.RuntimeException: test
  • when(スタブ化したいメソッドの呼び出し) の方法は、スタブ化したいメソッドが戻り値を持つことが前提となっている
    • 戻り値が void のメソッドだと、 when() に渡してもコンパイルエラーになる
  • 戻り値が void のメソッドが例外をスローするようにスタブ化したい場合は、 doThrow() から始まるスタブ化の方法を利用する
    • doThrow() の引数に、スローさせたい例外を設定する
    • 続けて、 when() の引数にモックオブジェクトを渡す
    • when() の戻り値の型は引数で渡したモックオブジェクトと同じ型になるようになっているので、スタブ化したいメソッド呼び出しをメソッドチェーンで続けて実行する

メソッドが呼び出されたことを検証する

package sandbox.mockito;

import org.junit.jupiter.api.Test;

import java.util.List;

import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;

public class VerifyTest {
    @Test
    void test() {
        List<String> mock = mock(List.class);

        mock.get(1);

        verify(mock).get(0);
    }
}
実行結果
Argument(s) are different! Wanted:
list.get(0);
-> at sandbox.mockito.VerifyTest.test(VerifyTest.java:17)
Actual invocations have different arguments:
list.get(1);
-> at sandbox.mockito.VerifyTest.test(VerifyTest.java:15)
  • Mockito#verify(T) メソッドを使用することで、モックのメソッドが1 回だけ実行されたことを検証できる
    • verify() メソッドの引数にモックオブジェクトを渡す
    • verify() の戻り値の型は引数で渡したモックオブジェクトと同じ型になっており、メソッドチェーンでメソッドを実行することで、そのメソッドが実行されたかどうかを検証できる
    • 検証は引数を含めて行われる
  • 上記実装例では、 get(0) が 1 回だけ実行されたことを検証している
    • 1 回も実行されていないのでエラーになる
    • get(0) が 2 回実行されている場合もエラーになる

引数のマッチング

    @Test
    void testVerifyWithArgumentMatcher() {
        List<String> mock = mock(List.class);

        mock.add("one");
        mock.add("two");
        mock.add("three");

        verify(mock).add(startsWith("th")); // テストOK
    }
  • verify() でも、 when() のとき と同じように引数のマッチングができる
  • 上記実装の場合、 add() メソッドに th で始まる値が渡されて実行されていればテストが通る

メソッドが実行された回数を検証する

    @Test
    void testVerifyInvocationCount() {
        List<String> mock = mock(List.class);

        mock.get(0);
        mock.get(0);
        mock.get(0);

        verify(mock, times(2)).get(0);
    }
実行結果
list.get(0);
Wanted 2 times:
-> at sandbox.mockito.VerifyTest.testVerifyInvocationCount(VerifyTest.java:39)
But was 3 times:
-> at sandbox.mockito.VerifyTest.testVerifyInvocationCount(VerifyTest.java:35)
  • verify() の第二引数に Mockito#times(int) を渡すことで、メソッドが実行された回数を検証できる
    • times() が指定されていない場合のデフォルトは times(1) を指定しているのと同じになる
  • 上記実装の場合、 get(0) が 2 回呼びだされていることを検証している
    • 3 回実行されているのでエラーになる
    • 0 回や 1 回の実行でもエラーになる

メソッドが最低 n 回実行されていることを検証する

    @Test
    void testAtLeast() {
        List<String> mock = mock(List.class);

        mock.get(0);

        verify(mock, atLeast(2)).get(0);
    }
実行結果
list.get(0);
Wanted *at least* 2 times:
-> at sandbox.mockito.VerifyTest.testAtLeast(VerifyTest.java:48)
But was 1 time:
  • Mockito#atLeast(int) を使用すると、メソッドが最低 n 回実行されたことを検証できる
  • 上記実装では、 get(0) が最低 2 回実行されていることを検証している
    • 1 回しか実行されていないのでエラーになる
    • 2 回以上実行されていれば OK
  • atLeast(1)Mockito#atLeastOnce() というエイリアスのメソッドが用意されている

メソッドが多くても n 回までしか呼び出されていないことを検証する

    @Test
    void testAtMost() {
        List<String> mock = mock(List.class);

        mock.get(0);
        mock.get(0);
        mock.get(0);

        verify(mock, atMost(2)).get(0);
    }
実行結果
Wanted at most 2 times but was 3
org.mockito.exceptions.verification.MoreThanAllowedActualInvocations: 
Wanted at most 2 times but was 3
  • Mockito#atMost(int) を使用すると、メソッドが多くても n 回までしか実行されていないことを検証できる
  • 上記実装では、 get(0) が多くても 2 回までしか実行されていないことを検証している
    • 3 回実行されているのでエラーになる
    • 0 ~ 2 回の実行であれば OK
  • atMost(1)Mockito#atMostOnce() というエイリアスのメソッドが用意されている

メソッドが 1 回も実行されていないことを検証する

    @Test
    void testNever() {
        List<String> mock = mock(List.class);

        mock.get(0);

        verify(mock, never()).get(0);
    }
実行結果
list.get(0);
Never wanted here:
-> at sandbox.mockito.VerifyTest.testNever(VerifyTest.java:68)
But invoked here:
-> at sandbox.mockito.VerifyTest.testNever(VerifyTest.java:66) with arguments: [0]
  • Mockito#never() を使用すると、メソッドが 1 回も実行されていないことを検証できる
  • 上記実装では、 get(0) が 1 回も実行されていないことを検証している
    • 1 回実行されているのでエラーになる

メソッドの実行順を検証する

    @Test
    void testInvocationOrder() {
        List<String> mock = mock(List.class);

        mock.get(0);
        mock.get(2);
        mock.get(1);

        InOrder inOrder = inOrder(mock);

        inOrder.verify(mock).get(0);
        inOrder.verify(mock).get(1);
        inOrder.verify(mock).get(2);
    }
実行結果
Verification in order failure
Wanted but not invoked:
list.get(2);
-> at sandbox.mockito.VerifyTest.testInvocationOrder(VerifyTest.java:86)
Wanted anywhere AFTER following interaction:
list.get(1);
-> at sandbox.mockito.VerifyTest.testInvocationOrder(VerifyTest.java:80)
  • Mockito#inOrder(モックオブジェクト)InOrder を取得し、 InOrderverify() を使ってメソッドの呼び出しを検証することで、メソッドの呼び出し順序を検証できる
  • 上記実装では、 get(0) -> get(1) -> get(2) の順番でモックのメソッドが実行されていることを検証している
    • 2 つ目の呼び出しが get(2) で異なっているため、エラーになっている

複数のモックに対するメソッドの実行順序を検証する

    @Test
    void testInvocationOrderWithMultipleMocks() {
        List<String> mock1 = mock(List.class);
        List<String> mock2 = mock(List.class);

        mock1.get(0);
        mock1.get(2);
        mock2.get(1);

        InOrder inOrder = inOrder(mock1, mock2);

        inOrder.verify(mock1).get(0);
        inOrder.verify(mock2).get(1);
        inOrder.verify(mock1).get(2);
    }
実行結果
Verification in order failure
Wanted but not invoked:
list.get(2);
-> at sandbox.mockito.VerifyTest.testInvocationOrderWithMultipleMocks(VerifyTest.java:102)
Wanted anywhere AFTER following interaction:
list.get(1);
-> at sandbox.mockito.VerifyTest.testInvocationOrderWithMultipleMocks(VerifyTest.java:96)
  • inOrder() の引数は可変長引数になっており、複数のモックオブジェクトを渡すことができる
  • 返された InOrder を使うと、引数で受け取ったモックオブジェクト達に対するメソッドの呼び出し順序を検証できる
  • 上記実装例では、 mock1.get(0) -> mock2.get(1) -> mock1.get(2) の順番で呼び出されていることを検証している
    • 2 つ目の呼び出しが mock1.get(2) となっているため、エラーとなっている

実際に渡された引数を取得する

    @Test
    void testCaptor() {
        List<String> mock = mock(List.class);

        mock.get(0);
        mock.get(1);
        mock.get(9);

        ArgumentCaptor<Integer> captor = ArgumentCaptor.forClass(Integer.class);

        verify(mock, times(3)).get(captor.capture());

        System.out.println("captor.getValue()     = " + captor.getValue());
        System.out.println("captor.getAllValues() = " + captor.getAllValues());
    }
実行結果
captor.getValue()     = 9
captor.getAllValues() = [0, 1, 9]
  • ArgumentCaptor を使用すると、モックのメソッドに渡された実際の引数を取得(キャプチャ)できる
  • ArgumentCaptor#forClass(Class) で、キャプチャしたい引数用の ArgumentCaptor を生成する
    • Class は、キャプチャしたい引数の型に合わせる
  • 生成した ArgumentCaptorcapture() メソッドを、 verify() による検証の中でキャプチャしたい引数の場所で実行する
  • キャプチャした結果は、 getValue() または getAllValues() で取得できる
    • getValue() は最後にキャプチャした値を返す
    • getAllValues() は、メソッドが複数回呼ばれた場合にそれぞれの呼び出しで渡された引数の値をリストで取得できる
      • メソッドが呼び出された順番でリストに設定されている

スパイ

package sandbox.mockito;

import org.junit.jupiter.api.Test;

import java.util.ArrayList;
import java.util.List;

import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.when;

public class SpyTest {

    @Test
    void test() {
        Hoge hoge = new Hoge();
        Hoge spy = spy(hoge);

        when(spy.size()).thenReturn(100);

        spy.add("hello");
        spy.add("world");
        spy.setName("Spied!");

        System.out.println("spy       = " + spy);
        System.out.println("spy.size  = " + spy.size());
        System.out.println("hoge      = " + hoge);
        System.out.println("hoge.size = " + hoge.size());
    }

    public static class Hoge implements Cloneable {
        private List<String> list = new ArrayList<>();
        private String name;

        public void setName(String name) {
            this.name = name;
        }

        public void add(String value) {
            list.add(value);
        }

        public int size() {
            return list.size();
        }

        @Override
        public String toString() {
            return "Hoge{" + "list=" + list + ", name='" + name + '\'' + '}';
        }

        @Override
        protected Object clone() throws CloneNotSupportedException {
            throw new CloneNotSupportedException();
        }
    }
}
実行結果
spy       = Hoge{list=[hello, world], name='Spied!'}
spy.size  = 100
hoge      = Hoge{list=[hello, world], name='null'}
hoge.size = 2
  • spy(T) メソッドに任意のオブジェクトを渡すことで、スパイを作成できる
  • スパイのメソッドは、スタブ化されていない場合は実際のメソッドの処理を実行する
    • 上の実装例では、スタブ化している size() 以外は実際のメソッドが実行されている
    • 一部のメソッドだけをスタブ化したい場合に利用できる
  • スパイはオリジナルのオブジェクトのコピーを元に作成されている
    • スパイ自体の状態を変更しても、オリジナルのオブジェクトの状態は変更されない
      • 上の実装例では、 name の状態は別々になっている
    • ただしディープコピーではないので、別オブジェクトへの参照を持つ場合は共有される
      • 上の実装例では、 list の状態が共有されている
    • コピーに clone() は使われていない
      • もし clone() でコピーしていたら、上の実装は落ちるはず

実際のメソッドを呼ばずにスタブ化する

先に失敗例。

    @Test
    void testStubbingSpyWithError() {
        List<String> list = new ArrayList<>();
        List<String> spy = spy(list);

        when(spy.get(0)).thenReturn("spied");

        System.out.println("spy.get(0) = " + spy.get(0));
    }
  • List のスパイを作成し、 get(0) の結果をスタブ化しようとしている
実行結果
java.lang.IndexOutOfBoundsException: Index 0 out of bounds for length 0
  • IndexOutOfBoundsException がスローされる
    • when(spy.get(0)) の時点では、まだ get(0) はスタブ化されていない
    • このため、 get(0) は実際のメソッドが実行されてしまい、 IndexOutOfBoundsException がスローされてしまっている
  • このように、スパイのメソッドをスタブ化しようとすると、 when(<メソッド呼び出し>) だと実際のメソッドが実行されてしまい不都合が発生することがある
  • この問題は、以下のように doReturn() を使うことで回避できる
    @Test
    void testStubbingSpyNoError() {
        List<String> list = new ArrayList<>();
        List<String> spy = spy(list);

        doReturn("spied").when(spy).get(0);

        System.out.println("spy.get(0) = " + spy.get(0));
    }
実行結果
spy.get(0) = spied
  • doReturn(<戻り値>) で始めて、続けて when(<スパイオブジェクト>) とする
  • when() の戻り値の型は引数で渡したスパイオブジェクトの型になっているので、そのままスタブ化したいメソッドを呼び出す
    • ArgumtnetMatchers を使って引数のマッチ条件を指定することも可能
  • こうするとスパイオブジェクト自体のメソッドを実行することなくスタブを定義できるため、先の問題が回避できる
  • スパイのメソッドを実行せずに例外をスローするようにスタブ化させたい場合は、 doThrow() を使用する

doReturn().when().xxx() の方法の場合、 doReturn() の引数に渡す値の型と最後に呼び出すスタブ化したいメソッドの戻り値の型は一致していなくてもコンパイルエラーにならない。

コンパイルエラーにならない例
List<String> mock = mock(List.class);
doReturn(10).when(mock).get(0); // get() の戻り値は String なのに、 doRetrun() には int を渡せる

この場合、実行時に ClassCastException がスローされてエラーになる。

このため、スパイにあるような特別な理由がない限りは、 doReturn().when().xxx() よりも when(mock.xxx()).doReturn() の書き方を使うことが推奨されている。

抽象クラスのスパイ

package sandbox.mockito;

import org.junit.jupiter.api.Test;

import static org.mockito.Mockito.spy;

public class AbstractSpyTest {

    @Test
    void test() {
        AbstractClass mock = spy(AbstractClass.class);
        System.out.println("mock.concreteMethod()       = " + mock.concreteMethod());
        System.out.println("mock.abstractStringMethod() = " + mock.abstractStringMethod());
    }

    public static abstract class AbstractClass {

        public String concreteMethod() {
            return "concreteMethod";
        }

        abstract public String abstractStringMethod();
    }
}
実行結果
mock.concreteMethod()       = concreteMethod
mock.abstractStringMethod() = null
  • 通常のスパイの生成には元となるインスタンスが必要だが、 Class オブジェクトを受け取る spy(Class) を使うと元のインスタンス無しでスパイを生成できる
  • 抽象クラスのように、元のインスタンス生成が面倒なクラスのスパイを用意するときに利用できる
    • 抽象メソッドの戻り値は、モックオブジェクトのデフォルトの戻り値と同じ値が返される
  • スパイ対象のクラスにはデフォルトコンストラクターが必要

本物のオブジェクトに委譲する形でモック(スパイ)化する

package sandbox.mockito;

import org.junit.jupiter.api.Test;

import static org.mockito.AdditionalAnswers.delegatesTo;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;

public class DelegateTest {
    @Test
    void test() {
        HogeFunction mock = mock(HogeFunction.class, delegatesTo(new FinalClass()));

        when(mock.world()).thenReturn("mocked");

        System.out.println(mock.hello(10));
        System.out.println(mock.world());
    }

    public interface HogeFunction {
        String hello(int n);
        String world();
    }

    public final static class FinalClass {
        public String hello() {
            return "hello()";
        }

        public String hello(int n) {
            return "hello(" + n + ")";
        }

        public String world() {
            return "world()";
        }
    }
}
実行結果
hello(10)
mocked
  • mock(Class, AdditionalAnswers.delegatesTo(<委譲先オブジェクト>)) とすることで、各メソッドの処理を委譲先オブジェクトに委譲するモックを作成できる
  • 委譲先のオブジェクトがモックの型を継承している必要はない
    • シグネチャが一致するメソッドに委譲される
    • 委譲先のメソッドがない場合は実行時エラーになる
  • これは、以下のようなオブジェクトやメソッドをモック/スタブ化したい場合に役立つ
    • final なクラスやメソッド
      • mockito は、標準では final なクラス/メソッドはモック/スタブ化できない1
    • すでにプロキシされているオブジェクト
      • Spring の Bean とかを指している?

モックをシリアライズ可能にする

package sandbox.mockito;

import org.junit.jupiter.api.Test;

import java.io.Serializable;
import java.util.List;

import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.withSettings;

public class SerializableTest {

    @Test
    void test() {
        List<String> noSerializableMock = mock(List.class);
        System.out.println(noSerializableMock instanceof Serializable);

        List<String> serializableMock = mock(List.class, withSettings().serializable());
        System.out.println(serializableMock instanceof Serializable);
    }
}
実行結果
false
true
  • mock() メソッドの第二引数に Mockito.withSettings().serializable() を渡すと、生成されたモックはシリアライズ可能になる

ワンライナーでモックを生成してスタブを定義する

package sandbox.mockito;

import org.junit.jupiter.api.Test;

import java.util.List;

import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;

public class OneLinerTest {
    List<String> mock = when(mock(List.class).get(0)).thenReturn("mocked").getMock();

    @Test
    void test() {
        System.out.println("mock.get(0) = " + mock.get(0));
    }
}
実行結果
mock.get(0) = mocked
  • thenReturn()/thenThrow() の後に getMock() でモックオブジェクトを取得できるので、モックの生成からスタブの定義までワンライナーで書ける

アノテーションで定義する

package sandbox.mockito;

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.*;

import java.util.ArrayList;
import java.util.List;

import static org.mockito.Mockito.*;

public class AnnotationTest {

    @Mock List<String> mock;
    @Spy List<String> spy = new ArrayList<>();
    @Captor ArgumentCaptor<String> captor;

    @BeforeEach
    void setUp() {
        MockitoAnnotations.openMocks(this);
    }

    @Test
    void test() {
        when(mock.get(0)).thenReturn("mocked");
        System.out.println("mock.get(0) = " + mock.get(0));

        spy.add("hello");
        spy.add("world");
        doReturn("spied").when(spy).get(0);
        System.out.println("spy.get(0) = " + spy.get(0));
        System.out.println("spy.get(1) = " + spy.get(1));

        verify(spy, atLeastOnce()).add(captor.capture());
        System.out.println("captor.getAllValues() = " + captor.getAllValues());
    }
}
実行結果
mock.get(0) = mocked
spy.get(0) = spied
spy.get(1) = world
captor.getAllValues() = [hello, world]
  • MockitoAnnotations#openMocks(<テストクラスのインスタンス>) を実行すると、アノテーションを使ってモックやスパイを定義できるようになる
  • テストクラスに宣言したフィールドにアノテーションをつけることで定義できる
    • @Mock でモックを定義できる
    • @Spy でスパイを定義できる
      • フィールドにオブジェクトが設定されている場合は、そのオブジェクトを元にスパイがつくられる
      • フィールドに何も設定されていない場合は、デフォルトコンストラクタで生成されたオブジェクトが使用される
        • たぶん、 spy(Class) を使った場合と同じ振る舞い
    • @CaptorArgumentCaptor を定義できる

JUnit 5 用の拡張

build.gradle
dependencies {
    // testImplementation "org.mockito:mockito-core:4.6.1"
    testImplementation "org.mockito:mockito-junit-jupiter:4.6.1"
    testImplementation "org.junit.jupiter:junit-jupiter:5.8.2"
}
package sandbox.mockito;

import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;

import java.util.List;
import java.util.Map;

import static org.mockito.Mockito.when;

@ExtendWith(MockitoExtension.class)
public class JUnit5ExtensionTest {
    @Mock List<String> mockList;

    @Test
    void test() {
        when(mockList.get(0)).thenReturn("mocked");
        System.out.println("mockList.get(0) = " + mockList.get(0));
    }

    @Test
    void testMethodArgument(@Mock Map<String, String> mockMap) {
        when(mockMap.get("hello")).thenReturn("mocked");
        System.out.println("mockMap.get(hello) = " + mockMap.get("hello"));
    }
}
実行結果
mockList.get(0) = mocked
mockMap.get(hello) = mocked
  • mockito-junit-jupiter を依存関係に追加すると、 MockitoExtension という JUnit 5 用の拡張クラスが使えるようになる
  • MockitoExtension を使用すると、 MockitoAnnotations#openMocks(Object) を自動でやってくれるようになる
  • また、メソッド引数でモックを受け取ることもできるようになる

inline mock maker

inline mock maker という拡張を導入すると、通常の mockito では実現できない final クラスのモック化や static メソッドのスタブ化などができるようになる。

inline mock maker は、 org.mockito:mockit-inline の依存関係を設定するだけで使えるようになる。

build.gradle
dependencies {
//    testImplementation "org.mockito:mockito-core:4.6.1"
    testImplementation "org.mockito:mockito-inline:4.6.1"
    testImplementation "org.junit.jupiter:junit-jupiter:5.8.2"
}

final クラス/メソッドをモック/スタブ化する

package sandbox.mockito;

import org.junit.jupiter.api.Test;

import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;

public class MockFinalClassTest {

    @Test
    void test() {
        FinalClass mock = mock(FinalClass.class);

        when(mock.hello()).thenReturn("mocked");

        System.out.println("mock.hello() = " + mock.hello());
    }

    public static final class FinalClass {
        public String hello() {
            return "world";
        }
    }
}
実行結果
mock.hello() = mocked
  • inline mock maker を導入すると、それだけで final クラスやメソッドをモック/スタブ化できるようになる

static クラス/メソッドをモック/スタブ化する

package sandbox.mockito;

import org.junit.jupiter.api.Test;
import org.mockito.MockedStatic;
import org.mockito.Mockito;

public class StaticMethodTest {

    @Test
    void test() {
        System.out.println("Hoge.hello() = " + Hoge.hello("before"));

        try (MockedStatic<Hoge> mocked = Mockito.mockStatic(Hoge.class)) {
            mocked.when(() -> Hoge.hello("test")).thenReturn("mocked");

            System.out.println("Hoge.hello() = " + Hoge.hello("test"));

            mocked.verify(() -> Hoge.hello("test"));
        }

        System.out.println("Hoge.hello() = " + Hoge.hello("after"));
    }

    public static class Hoge {
        public static String hello(String arg) {
            return "world (arg=" + arg + ")";
        }
    }
}
実行結果
Hoge.hello() = world (arg=before)
Hoge.hello() = mocked
Hoge.hello() = world (arg=after)
  • Mockito#mockStatic(Class) で、スタブ化したい static メソッドを持つクラスを指定する
  • static メソッドのスタブ化が有効なのは、 mockStatic() の戻り値である MockedStaticclose() されるまでの間だけ
    • try-with-resources 文で確実に close() されるようにすることが推奨されている
  • static メソッドのスタブは、 MockedStaticwhen(Verification) メソッドで定義する
    • 引数にはスタブ化したい static メソッドの呼び出しを実装した Verification を渡す
    • Verification は関数型インタフェースなので、ラムダ式でスタブ化したいメソッドを呼び出すように書くのが楽
      • 引数がないメソッドであれば、メソッド参照で書くことも可能(mocked.when(SomeClass::staticMethod)
    • 戻り値が void のメソッドも、同じように when() でスタブ化する
      • mocked.doReturn(...).when(...) のような書き方はない
  • verify() も、 Mocked に用意されているメソッドを使って検証する

コンストラクタがモックを返すようにする

package sandbox.mockito;

import org.junit.jupiter.api.Test;
import org.mockito.MockedConstruction;

import static org.mockito.Mockito.mockConstruction;
import static org.mockito.Mockito.when;

public class MockConstructorTest {

    @Test
    void test() {
        System.out.println("[before] new Hoge().hello() = " + new Hoge().hello());

        try (MockedConstruction<Hoge> mocked = mockConstruction(Hoge.class)) {
            Hoge mock = new Hoge();

            when(mock.hello()).thenReturn("mocked");

            System.out.println("mock.hello() = " + mock.hello());
            System.out.println("new Hoge().hello() = " + new Hoge().hello());
        }

        System.out.println("[after] new Hoge().hello() = " + new Hoge().hello());
    }

    public static class Hoge {
        public String hello() {
            return "world";
        }
    }
}
実行結果
[before] new Hoge().hello() = world
mock.hello() = mocked
new Hoge().hello() = null
[after] new Hoge().hello() = world
  • Mockito#mockConstrcution(Class) で、 Class オブジェクトを渡したクラスのコンストラクタはモックオブジェクトを返すようになる
  • コンストラクタがモックを返すのは、 mockConstrcution() の戻り値である MockedConstructionclose() されるまでの間だけ
    • try-with-resources 文で確実に close() させることが推奨されている
  • スタブ化や検証は、通常の Mockito の利用方法と同じで when(), verify() を使う

モックを初期化する

    @Test
    public void test2() {
        try (final MockedConstruction<Hoge> mocked = mockConstruction(Hoge.class, (mock, ctx) -> {
            when(mock.hello()).thenReturn("mocked");
        })) {
            final Hoge hoge = new Hoge();
            System.out.println(hoge.hello());
        }
    }
実行結果
mocked
  • よそのメソッド内部などで new で生成されるモックオブジェクトに対して初期化を行いたい場合は、 mockConstruction の第二引数に MockInitializer を渡す
  • 第一引数に外部で生成されたモックオブジェクトが渡されるので、他と同様の方法で操作を記録することができる

モック化できないクラスを定義する

DoNotMock
package sandbox.mockito.org.mockito;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface DoNotMock {
}
  • DoNotMock という名前のアノテーションを、 <任意のパッケージ>.org.mockito パッケージの下に作成する
  • @TargetTYPE にして、 @RetentionRUNTIME にする
package sandbox.mockito;

import org.junit.jupiter.api.Test;
import sandbox.mockito.org.mockito.DoNotMock;

import static org.mockito.Mockito.mock;

public class DoNotMockTest {

    @Test
    void test() {
        mock(Hoge.class);
    }

    @DoNotMock
    public static class Hoge {}
}
  • Hoge クラスに @DoNotMock アノテーションを設定し、モック化を試みる
実行結果
org.mockito.exceptions.misusing.DoNotMockException: class sandbox.mockito.DoNotMockTest$Hoge is annotated with @org.mockito.DoNotMock and can't be mocked.
  • モック化に失敗する
  • <任意のパッケージ>.org.mockito.DoNotMock という名前のアノテーションを作成してクラスに設定しておくと、そのクラスのモック化を禁止できる

参考

  1. 後述する inline mock maker という拡張機能を使用すると可能

54
48
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
54
48