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
にすると、スタブとなるモックの引数ミスマッチも検出してくれるので、一番厳しく設定。
- Mockitoには
- 初期化は原則アノテーションで行う。
-
mock(XX.class)
やspy(new XX())
,ArgumentCaptor.forClass(XX.class)
を使った初期化方法もあるが、アノテーション使ったほうがシンプルに書ける。 -
@InjectMocks
を付与する対象はインターフェースにしないよう注意。(newできない)
-
-
verify
は書かなくてよい。- v2からは用意したモックスタブが呼ばれなかった場合、非チェック例外が発生するので、呼ばれたかいちいち
verify
で検証する必要はないと思う。 - あるとすれば、複数回呼ばれた回数をカウントしたい場合くらい?
- v2からは用意したモックスタブが呼ばれなかった場合、非チェック例外が発生するので、呼ばれたかいちいち
- モックの記述は
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()
形式での記述が必要になる。
前準備
- テストでMockitoを使うためにおまじない
@RunWith(MockitoJUnitRunner.StrictStubs.class)
テストクラスに付与する。-
@Rule
で同様の設定をしても良い。(Strictness.STRICT_STUBS
)
-
- テスト対象となるクラスの変数に
@InjectMocks
を付与する。 - モック化したいクラスの変数に
@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;
...
}
テストケース
-
@Test
を付与したvoid publicメソッドを定義する。 - モックインスタンスのモック化したいメソッドの定義を記述する。
- テスト対象のメソッドを呼ぶ。
- 結果を検証する。
テスト対象が以下のとき、
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);
参考