JUnitで使うMockitoの設定をテストごとにわかりやすく変える

動作確認環境

  • Java 11
  • JUnit 5.7.2

関連記事

JUnitで使うDTOの各フィールド値のバリエーションをPure Javaで量産する

Mockの振る舞いを事細かに変えるユニットテストの書き方と問題点

ユニットテストで大量のバリエーションをテストするときに問題になる点についてJUnitで使うDTOの各フィールド値のバリエーションをPure Javaで量産するで記載したのだが、Mockitoでも同様のことが言える。

ユニットテストのテスト対象メソッドに渡す引数では以下のように問題点を3つあげた。

実用上は特段問題ないが、以下の点でテストコードが読みづらい、扱いづらいと感じる。

  1. dtoの初期化を忘れないようにしないといけない
  2. dtoの初期化をせずに前のケースの状態 + 特定のsetterを呼ぶような前のケースに依存したコードを書くと、コードが追いづらい
  3. dtoの生成の始まりから終わりが判別しづらい

1と2はとくに補足は不要だと思う。

3について空行を入れるか、あるいはブロックで囲むという対策が取れると思う。(メソッド自体を分けるというのは無しとする。理由はバリエーション自体が大量にあるという前提なので、その分大量にメソッドを作るのが嫌だから。)

Mockitoの振る舞いを変える場合も同じ問題点を挙げられる。

  1. mockの初期化を忘れないようにしないといけない
  2. mockの初期化をせずに前のケースの状態 + 特定のmockの変更をするような前のケースに依存したコードを書くと、コードが追いづらい
  3. mockの設定の始まりから終わりが判別しづらい

モックの振る舞いを少しずつ変えながらテスト実行していくコードを書く。

@Mock
XxxService xxxService;

private void initMock() {
    doReturn(1).when(xxxService).methodX();
    doReturn(true).when(xxxService).methodY();
    doReturn(List.of(1)).when(xxxService).methodZ();
}

@Test
public void test() {
    // 正常系その1
    initMock();
    // exec test and assert. 略

    // 正常系その2
    initMock();
    doReturn(100).when(xxxService).methodX();
    // exec test and assert. 略

    // 正常系その3
    initMock();
    doReturn(false).when(xxxService).methodX();
    doReturn(List.of(1, 2)).when(xxxService).methodZ();
    // exec test and assert. 略
}

lambdaを使った対応

JUnitで使うDTOの各フィールド値のバリエーションをPure Javaで量産するのときと同様に、ブロックでくくる手法を応用して、文法上ブロックが出てくるのが必然(もしくは自然)だし、1行しかブロック内になくても違和感のないコードを書いてみたい。

また、mockの初期化も忘れることがありえないようにしてみたい。

コードを記載するが、ポイントはinitMockの引数で関数を受け取り、DTOの初期化のあとで関数を実行するというもの。

@Mock
XxxService xxxService;

@Test
public void test() {
    Consumer<Runnable> initMock = override -> {
        doReturn(1).when(xxxService).methodX();
        doReturn(true).when(xxxService).methodY();
        doReturn(List.of(1)).when(xxxService).methodZ();
        if (override != null) {
            override.run();
        }
    };

    // 正常系その1
    initMock.accept(null);
    // exec test and assert. 略

    // 正常系その2
    initMock.accept(() -> doReturn(100).when(xxxService).methodX());
    // exec test and assert. 略

    // 正常系その3
    initMock.accept(() -> {
        doReturn(false).when(xxxService).methodX();
        doReturn(List.of(1, 2)).when(xxxService).methodZ();
    });
    // exec test and assert. 略
}

privateメソッドを避ける

先程書いたコードでは、あえてprivateメソッドでinitMockを定義しなかった。

DTOの生成なら、他のテストメソッドから共用されることも多く、再利用可能なかたちで定義することにメリットがある。また、DTOのフィールド数はかなり多くなることがあるので、テストメソッド内で全フィールドのセットを行うとコードの見通しが悪くなる可能性もある。

対してモックは、他のテストメソッドで共用しない場合も多く、またモック数もそれほど多くない。(逆にものすごく大量のモックの設定をしなければならないならプロダクトコードの設計自体に問題があるかもしれない)

initMockをprivateメソッドに切り出さず一テストメソッド内にベタがきした方が、「何の設定を上書きするのか」など却って読みやすくなる。