はじめに
ひとりJUnitアドベントカレンダー9日目の記事です。
今回は二日前と一日前の記事に入れ漏れたMockitoの小ネタです。
使用バージョン
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.2.6.RELEASE</version>
$ mvn dependency:tree | grep mockito
[INFO] | +- org.mockito:mockito-junit-jupiter:jar:3.1.0:test
[INFO] | +- org.mockito:mockito-core:jar:3.1.0:test
前提
以下のようなクラス構成を想定します。
@Service
@RequiredArgsConstructor
public class HogeService {
@NonNull
private MogeService mogeService;
public void execute() {
System.out.println(mogeService.returnStr());
System.out.println(mogeService.returnStr());
}
}
@Service
public class MogeService {
public String returnStr() {
return new Random().nextBoolean() ? "foo" : "bar";
}
}
呼出ごとにMockの挙動を変える
HogeService
はMogeService
のreturnStr()
を二回呼んでいます。
テストの中でMogeService
をmock化したとして、
呼び出されたreturnStr()
の返却値を設定するためには以下のように書きます。
when(mogeService.returnStr()).thenReturn("foo");
この書き方をすると、何回呼び出されたとしても常に"foo"
を返却します。
では、一回目は"foo"
を、二回目は"bar"
を返すような設定としたい場合はどうでしょう。
または、一回目は"foo"
を返却、二回目は例外を投げさせたい場合はどうでしょうか。
以下です。
// foo -> bar(チェーン)
when(mogeService.returnStr()).thenReturn("foo").thenReturn("bar");
// foo -> bar(可変長引数利用)
when(mogeService.returnStr()).thenReturn("foo", "bar");
// foo -> 例外
when(mogeService.returnStr()).thenReturn("foo").thenThrow(new NullPointerException());
単純にチェーンさせるだけです。もしくは可変長引数なので複数渡す形でも。
ちなみにこの設定で三回目を呼ぶと、上なら"bar"
、下ならぬるぽになります。
最後に設定した値が残存するようなイメージですね。
thenThrow()
の指定方法
上の話で思い出したので一つ。
mock呼出時に例外を発生させるthenThrow()
あるいはdoThrow()
ですが、
インスタンスを生成して引数に渡す方法の他に、例外のクラスを渡す方法もあります。
when(mogeService.returnStr()).thenThrow(new NullPointerException());
when(mogeService.returnStr()).thenThrow(NullPointerException.class);
インスタンス化するのが面倒な例外とかだと使い所ありそうですね。
when(mogeService.returnStr()).thenThrow(WebServiceIOException.class);
呼び出し回数の検証方法
mock化したクラスのメソッドが呼ばれた回数は以下の記述で検証ができます。
verify(mogeService, times(2)).returnStr();
今回想定するテスト対象は以下なので、上記テストは通ります。
public void execute() {
System.out.println(mogeService.returnStr());
System.out.println(mogeService.returnStr());
}
回数を厳密に指定する以外にも、以下のような検証も可能です。
// 最低1回は呼び出されること。引数の数字を変えれば最低2回、3回なども可能
verify(mogeService, atLeast(1)).returnStr();
// 最低1回の検証は専用のメソッドを使ってもOK
verify(mogeService, atLeastOnce()).returnStr();
// 最大3回まで呼び出されること
verify(mogeService, atMost(3)).returnStr();
// 最大でも1回まで呼び出されること。今回のケースだと落ちる
verify(mogeService, atMostOnce()).returnStr();
また、以下のようなメソッド単位ではなくクラス単位の検証もあります。
// mogeServiceの全てのメソッドが一度も呼ばれていないこと
verifyNoInteractions(mogeService);
// mogeServiceのメソッドがこれ以上呼ばれていないこと
verifyNoMoreInteractions(mogeService);
verifyNoInteractions()
はまあ名前の通り、何も呼ばれていないよねという検証ですが
verifyNoMoreInteractions()
は若干複雑で、
「すべての呼び出しをちゃんと検証しきれているか?」を検証するメソッドです。
@Test
void テスト() {
hogeService.execute();
// ここでは落ちる
verifyNoMoreInteractions(mogeService);
// MogeServiceのメソッドが呼ばれていることを検証
// hogeService.execute()はreturnStr()以外は呼ばないので、これだけで全て検証完了
verify(mogeService, times(2)).returnStr();
// 検証完了しているため、ここでは通る
verifyNoMoreInteractions(mogeService);
}
メソッドが多いクラスをmock化した場合、「検証していないものがないこと」の検証は
自分で書こうとするとなかなか大変なものです。
「どのメソッドも一度も呼ばれていないこと」も同じで、
全てのメソッドをverify(mock, times(0))
するのも馬鹿馬鹿しいですし、
せっかくライブラリ側が用意してくれているので、適切に使ってできるだけラクしたいですね。