LoginSignup
43
58

More than 5 years have passed since last update.

Mockitoの記述方法

Last updated at Posted at 2019-04-13

JUnit + Mockitoでテストする際のMockitoの記述方法が紹介するページによりマチマチなので自分なりに整理した。

環境

  • Mockito v2.23.9
  • JUnit v4.12(JUnit5にしたい)

Mockitoの機能

Mock

対象インスタンスのメソッドをすべてモック化して置き換えたい場合に使用する。
デフォルト状態では、オブジェクトの場合、戻り値があるメソッドはnullを返すようになる。(Collectionでは空のCollectionだったり、プリミティブな値では0とかfalseとか...)

@Mockのアノテーションを付与することでモック化できる。
また、テスト対象となるクラスのインスタンスに@InjectMocksを付与することで、
インスタンス内の@Injectされたメンバーのインスタンスに@Mockのモックインスタンスを差し込むことができる。

Spy

対象のインスタンスの特定のメソッドだけを置き換えたい場合に使用する。
私はあまり使いこなせていない。
getterやらのメソッドはそのまま使いつつ1つのメソッドだけ置き換えたい場合とかがある?

@Spyのアノテーションを付与することで利用できる。
インスタンスは自分で生成する必要がある。
@InjectMocksで注入することはできない。

Captor

主に引数の値をキャプチャして検証するのに使用する。
引数がオブジェクトの場合、eqのような標準のマッチャでは検証できない。
このとき、Captorが有効である。

引数をキャプチャする場合、ArgumentCaptorクラスのインスタンスを定義し@Captorを付与する。

基本のテストの書き方

DIを利用した設計をしていることを前提に、Mockitoを使った基本的な記述をまとめる。

基本方針

  • MockitoのモードはStrictStubsにする。
    • MockitoにはSilent, Strict(v2のデフォルト), StrictStubsの3つのモードがある。
    • StrictStubsにすると、スタブとなるモックの引数ミスマッチも検出してくれるので、一番厳しく設定。
  • 初期化は原則アノテーションで行う。
    • mock(XX.class)spy(new XX()), ArgumentCaptor.forClass(XX.class)を使った初期化方法もあるが、アノテーション使ったほうがシンプルに書ける。
    • @InjectMocksを付与する対象はインターフェースにしないよう注意。(newできない)
  • verifyは書かなくてよい。
    • v2からは用意したモックスタブが呼ばれなかった場合、非チェック例外が発生するので、呼ばれたかいちいちverifyで検証する必要はないと思う。
    • あるとすれば、複数回呼ばれた回数をカウントしたい場合くらい?
  • モックの記述はdoXX().when()の形式で書く。
    • ここは賛否両論あり、公式も態度明らかにしていない感じである。 you may prefer to use these methods in place of the alternative with when(), for all of your stubbing calls. とのこと。
    • 個人的には、戻り値の有無やMockとSpyの違いで記述方法をいちいち覚えるのは面倒。見た目も煩雑になると思う。
    • よって、すべての記述で使えるdoXX().when()の形式に統一する。
    • ただし、後述する呼び出しによって戻り値を変えるケースではwhen().thenReturn()形式での記述が必要になる。

前準備

  1. テストでMockitoを使うためにおまじない@RunWith(MockitoJUnitRunner.StrictStubs.class)テストクラスに付与する。
    • @Ruleで同様の設定をしても良い。(Strictness.STRICT_STUBS)
  2. テスト対象となるクラスの変数に@InjectMocksを付与する。
  3. モック化したいクラスの変数に@Mockを付与する。
    • @InjectMocksおよび@Mockのアノテーションを付与するだけでインスタンスが生成される。
import org.junit.runner.RunWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnitRunner;
...

@RunWith(MockitoJUnitRunner.StrictStubs.class)
public class XXTest{
  @InjectMocks
  XXInteractor target;

  @Mock
  IXXRepository repoMock;

  ...
}

テストケース

  1. @Testを付与したvoid publicメソッドを定義する。
  2. モックインスタンスのモック化したいメソッドの定義を記述する。
  3. テスト対象のメソッドを呼ぶ。
  4. 結果を検証する。

テスト対象が以下のとき、

public class XXInteractor{
  @Inject
  IXXRepository repository;

  XXEntity fetch(String id) throws NotFoundException{
    try{
      return repository.findBy(id);
    }
    catch(UseCaseNotFoundException e){
      throw new NotFoundException();
    }
  }
}

以下のようなテストが書ける。

  private static final String ID = "ID";

  @Test
  public void testFetchSuccess() throws Exception{
    // 引数にIDが与えられたときだけ正常なXXEntityを返す。(その他の場合、null)
    doReturn(new XXEntity(ID))
      .when(repoMock).findBy(eq(ID));

    XXEntity result = target.fetch(ID);

    assertThat(result.getId(), is(ID));
  }

  @Test(expected = NotFoundException.class)
  public void testFetchFailureWithNotFound() throws Exception{
    doThrow(new RepositoryNotFoundException())
      .when(repoMock).findBy(eq(ID));

    target.fetch(ID);
  }

モック化ユースケース

基本編

基本的なモック化パターンを以下に示す。

モック化したメソッドで値を返す

// 戻り値がある場合
doReturn(new XXEntity(ID))
  .when(targetMock).fetch(eq(ID));

// 戻り値がない場合
doNothing().when(targetMock).delete(eq(ID))

モック化したメソッドで例外を発生させる

doThrow(new XXException()).when(targetMock).fetch(eq(ID))

doThrow(new XXException()).when(targetMock).delete(eq(ID))

Spyしたインスタンスのメソッドを置き換える

// 戻り値を返す
doReturn(new XXEntity()).when(targetSpy).fetch()

// 例外を発生させる
doThrow(new XXException()).when(targetSpy).fetch()

応用編

どんな引数でも呼ばれたらレスポンスしたい

doReturn(new XXEntity(ID))
  .when(targetMock).fetch(any())

同じメソッドで呼び出しごとに違う値を返したい

// 引数がないケース、あるいは、同じ引数で呼び出しごとに別の値を返したいケース
// このケースはdoReturnでは書けないのでthenReturn形式で記述する。
when(targetMock.getNextKey())
  .thenReturn("ID-1")   // 一度目の呼び出し
  .thenReturn("ID-2");  // 二度目の呼び出し

// 引数ごとに違う値を返すケース(普通にそれぞれ定義)
doReturn(new XXEntity("ID-1")).when(targetMock).fetch(eq("ID-1");
doReturn(new XXEntity("ID-2")).when(targetMock).fetch(eq("ID-2");

プリミティブでない引数を検証したい

引数がプリミティブな場合はeqのみで検証できるが、引数がオブジェクトやリスト・マップ系の場合は検証できないため、Captorと組み合わせる必要がある。

テスト対象が以下のとき、

public class XXInteractor{
  @Inject
  IXXRepository repository;

  void update(XXEntity entity){
    repository.update(entity);
  }
}

以下のように検証する。

  private static final String ID = "ID";

  @InjectMocks
  XXUseCase target;

  @Mock
  IXXRepository repoMock;

  // Captorを定義
  @Captor
  ArgumentCaptor<XXEntity> argCaptor;

  @Test
  public void testUpdateSuccess(){
    // 引数をキャプチャするためにモックを定義
    doNothing().when(repoMock).update(argCaptor.capture());

    target.update(ID);

    // キャプチャした引数を検証
    assertThat(argCaptor.getValue().getId(), is(ID));
  }

モックに渡ってきた引数を書き換えたい

引数の一部を戻り値に使うC#でいうrefのような実装をしているメソッドをモックにするケース。
このメソッド定義はTestableでないので、根本的には設計変更で解決すべきだが、
どうしても引数でもらったインスタンスの中身を書き換えたい場合は以下の方法で実現できる。

doAnswer( invocation -> {
  Object[] args = invocation.getArguments();

  // 引数をそのクラスにキャストすることで、そのインスタンスを操作できるようになる。
  // このケースでは第二引数を書き換えるのでargs[1]
  ((XXOutputParam) args[1]).setId("ID");
  return null; //戻り値がvoidならnull
}).when(useCaseMock).get(any(), any())

モックの動きをリセットしたい

@Beforeのメソッドあたりで毎回呼ぶと良い。

reset(mock);

参考

43
58
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
43
58