Junitでのモック定義(Mockito)

Mock

1. Mockとは

テストしたいクラスが依存しているブジェクトに代わって使用されるテスト用のオブジェクト。
テストに必要な部品の値を擬似的に設定するもの。

例えば、クラスAのテストをしようとしたときに以下のような時があったら使用する

  • 現在日時を返すメソッドを使用するテスト
  • クラスBのメソッドを使用するがまだクラスBが完成していない
public class A {

    private B b;

    public A(B b){
        this.b = b;
    }

    // doSomethingメソッドはBオブジェクトに依存している
    // テストが成功するかどうかはdoSomethingメソッドの実装と
    // Bオブジェクトの実装の両方に依存している
    public String doSomething(String firstName, String lastName){
        String name = b.connect(firstName, lastName);
        int length = name.length;
        return "名前の文字数は" + length + "です";
    }
}

public class B implements C {
    @Override
    public String connect(String s1, String s2){
        // まだ処理が未実装
        return null;
    }
}

public interface C {
    String connect(String s1, String s2);
}

2. Mockを作成する目的(まとめ)

doSomethingメソッドのテストは、doSomethingメソッドの実装が正しい処理を示すかに注目すべき
⇒ Bオブジェクトの実装が正しい処理を示すかを検証する必要はない
⇒ テストはBオブジェクトの実装が正しい処理を示す前提で行いたい
その前提があることによって、テストが失敗した時、Bオブジェクトは正しい処理を示すとわかっているので、doSomethingメソッドの実装自体にテスト失敗の原因があることがわかる。

3. Mockの作り方

3.1 インターフェースのテスト用の実装を作成してモックとする方法

テスト対象のクラスが依存しているのがインターフェースの場合に取りうる方法。
別途OSSを追加することなく、モックを作成することが出来る。
テストメソッドごとに返却する値を変えたい場合などは、モックの実装が大変となるが、モックの実装は本来力を入れる部分ではないため、その場合は後述のMockitoの導入を検討するべき。

public class ATest {

    @Test
    public void doSomething(){
        A a = new A(new TestB());
        String actual = a.doSomething("山田", "太郎");
        assertEquals("名前の文字数は4です", actual);
    }

    private class TestB implements C {
        @Override
        public String connect(String s1, String s2){
            return s1 + s2;
        }
    }
}

3.2 Mockitoを使用する

◇ Mockitoでできること

  1. モックオブジェクトの作成
  2. モックオブジェクトのふるまい定義
  3. 部分的なモックオブジェクトの作成
  4. メソッド呼び出し回数の検証
  5. staticメソッドのモック化

1. 依存関係の追加

mockito-inlineはstaticメソッドやfinalクラスのモック化に必要となる

testImplementation 'org.mockito:mockito-core:3.6.28'
testImplementation 'org.mockito:mockito-inline:3.6.28'

2. モックオブジェクトの作成(Mock)

アノテーションを使用するか、メソッドを使用することでモックを作成することが出来る。
作成されたモックオブジェクトはすべてのメソッドで内部ロジックを持たない。
戻り値があるメソッドでは戻り値はデフォルト値となる。

アノテーションの使用

アノテーションを付与したメンバ変数は、これからモックにしますという宣言をしたことになる。
⇒モックオブジェクトを作成するには別途MockitoAnnotations.openMocks(this);という宣言が必要。

public class ATest {
    @Mock
    private B b;

    @BeforeEach
    public void setUp(){
        // このメソッドが実行されて初めて@Mockアノテーションを付与したメンバ変数に
        // モックオブジェクトが注入される
        MockitoAnnotations.openMocks(this);
    }
}
メソッドの使用

テストメソッド内でmock()メソッドを呼び出すことで作成する。
メソッドを呼び出すことですぐにモックオブジェクトを得られるが、テストメソッド毎に記述する必要がある。

public class ATest {

    @Test
    public void doSomething(){
        private B mockB = mock(B.class);
    }
}

3. モックのふるまい定義

whenメソッドを使用することで、メソッドのふるまいを定義することができる

public class ATest {

    @Test
    public void doSomething(){
        private B mockB = mock(B.class);
        // connectメソッドの引数が"abc"と"def"の場合のみ戻り値が"abcdef"になる
        when(mockB.connect("abc", "def")).thenReturn("abcdef");

        // connectメソッドの引数が何であっても戻り値が"abcdef"になる
        when(mockB.connect(anyString(), anyString())).thenReturn("abcdef");

        // connectメソッドの引数が何であっても例外が発生する
        when(mockB.connect(anyString(), anyString())).thenThrow(new IllegalArgumentException("テストテスト"));
    }
}

4. 部分的なモックオブジェクトの作成(Spy

アノテーションを使用するか、メソッドを使用する。
作成されたモックオブジェクトはすべてのメソッドで内部ロジックを持っている。
自分でwhenメソッドを使用してふるまいを定義したメソッドのみモック化される。

アノテーションの使用

アノテーションを付与したメンバ変数は、これからモックにしますという宣言をしたことになる。
⇒モックオブジェクトを作成するには別途宣言が必要

public class ATest {
    @Spy
    private B b;

    @BeforeEach
    public void setUp(){
        MockitoAnnotations.openMocks(this);
    }
}
メソッドの使用

テストメソッド内でメソッドを呼び出すことで作成する。

public class ATest {

    @Test
    public void doSomething(){
        private B mockB = spy(B.class);
    }
}

5. メソッド呼び出し回数の検証

verifyメソッドを使用することで、モック化したメソッドが何回実行されたかを検証することが出来る。

public class ATest {

    @Test
    public void doSomething(){
        private B mockB = mock(B.class);
        verify(mockB, times(1)).exists();
    }
}

6. staticメソッドのモック化

以下のように内部で使用しているstaticメソッドのふるまいを定義する方法について説明する。

public class SystemDate {

    public LocalDateTime getCurrent() {
        return LocalDateTime.now();
    }
}

staticメソッドをモック化する場合は、mockStaticメソッドを使用してモックオブジェクトを作成する。
作成されたモックオブジェクトに対する振る舞いの定義方法等は他のモックと同一である。
モック使用後はcloseメソッドでクローズする。

public class SystemDateImplTest {

    @ParameterizedTest
    @CsvSource({
            // システム日付
            "2020-01-01T00:00:00.000",
            "2019-12-31T23:59:59.999",
            "2021-07-07T12:34:56.789"
    })
    public void getCurrent_システム日付が返却されること(LocalDateTime current) {
        // currentの日時でシステム日付を固定
        var mock = Mockito.mockStatic(LocalDateTime.class);
        mock.when(LocalDateTime::now).thenReturn(current);

        LocalDateTime actual = new SystemDate().getCurrent();
        assertEquals(actual, current);

        mock.close();
    }
}

Junit5の使用方法も記事にしているのでよかったらご覧ください。 olafnosuke.hatenablog.com