はじめに
ひとりJUnitアドベントカレンダー8日目の記事です。
前日に引き続き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
前日記事でSpringboot古いとか書いたけどMockitoも4系最新なので大概ですね。
テスト対象クラスが他クラスに渡す引数
検証したい。
単体テストでテスト対象処理を単純に呼び出すだけだと、
その処理にある値を渡したらどういう結果が返ってくるかは取得できますが、
その処理が他の処理にどんな値を渡しているかは、普通は検証できません。
ただ、戻り値を返さないメソッドだったり、
複数の他処理をハブ的に呼び出すだけの処理であった場合は、
そもそもそんなクラスおかしいよって話になるのかもしれませんが、
「どうやって他の処理を呼び出しているのか」を検証したくなると思います。
もしくは「ちゃんと他の処理を想定回数呼び出せているのか」でもいいですが。
そこで、やはりMockitoです。
前提
やはり前日記事と同じく以下のような構成です。
テスト対象はHogeService
とします。
@Service
@RequiredArgsConstructor
public class HogeService {
@NonNull
private final MogeService mogeService;
public void execute() {
System.out.println(mogeService.returnStr());
mogeService.returnNothing();
System.out.println("hoge executed.");
}
}
@Service
public class MogeService {
public String returnStr() {
System.out.println("returnStr executed.");
return "moge return";
}
public void returnNothing() {
System.out.println("returnNothing executed.");
}
public String returnArg(String arg1, String arg2) {
return arg1 + arg2;
}
}
ご覧の通り、HogeService
のexecute()
では
MogeService
のreturnStr()
とreturnNothing()
を一度ずつ呼んでいます。
ただ、returnArg()
は一度も呼んでいません。
これを検証します。
早めの結論、verify
を使う
サクッと結論を書くと、以下です。
@Test
void テスト1() {
when(mogeService.returnStr()).thenReturn("mogemoge");
hogeService.execute();
verify(mogeService, times(1)).returnStr();
verify(mogeService, times(1)).returnNothing();
verify(mogeService, times(0)).returnArg(any(), any());
}
org.mockito.Mockito.verify
をstaticインポートして使っています。
verify
の引数にmockインスタンスとtimes()
を渡し、
times()
の引数には想定される呼び出し回数を渡します。
そこからさらに生やしたメソッドが回数検証対象の処理となります。
検証対象処理が引数を取る場合は渡してやる必要がありますが、
上記のany()
は語義通り「何でも」という意味なので、
この場合はどんな引数でもいいから呼び出された回数が合計何回なのかを数えます。
特定の引数の呼び出しだけ数えたい場合はeq()
で指定します。
// 第一引数も第二引数も何でもいいので、とにかく処理が呼ばれた回数
verify(mogeService, times(0)).returnArg(any(), any());
// 第一引数は何でもいいから、第二引数が"moge"で呼ばれた回数
verify(mogeService, times(0)).returnArg(any(), eq("moge"));
// 第一引数が"hoge"、第二引数が"moge"で呼ばれた回数
verify(mogeService, times(0)).returnArg(eq("hoge"), eq("moge"));
渡された引数が複雑なとき
eq()
を使えば「特定の引数で呼ばれた回数」をカウントすることができますが、
「処理の中で実際に渡された値」と「eq()
に渡した値」が、
equals()
で一致することが前提です。
つまり、以下のようなケースは困ります。
public class HogeService {
public void execute() {
Student student = new Student("太郎");
mogeService.returnByStudent(student);
}
}
public class MogeService {
public String returnByStudent(Student student) {
return student.toString();
}
}
HogeService
がMogeService#returnByStudent()
を呼び出しています。
returnByStudent()
はStudent
を引数に取るメソッドで、
HogeService
は文字列"太郎"
を元にStudent
をnewして処理に渡すような形です。
@Test
void テスト2() {
when(mogeService.returnByStudent(any())).thenReturn("太郎");
hogeService.execute();
Student expectedStudent = new Student("太郎");
verify(mogeService, times(1)).returnByStudent(eq(expectedStudent));
}
文字列"太郎"
を元にnewしたStudent
によって、
returnByStudent()
が1回呼び出されたことを検証するテストなので、一見通りそうです。
が、実際は失敗します。
Wanted but not invoked:
mogeService.returnByStudent(
sample.Student@53b7f657
);
However, there was exactly 1 interaction with this mock:
mogeService.returnByStudent(
sample.Student@55c53a33
);
Student
が独自のequals()
をオーバーライド実装していたり、
Lombokの@EqualsAndHashCode
が付与されていれば話は別ですが、
特に何もしていなければ、equals()
を呼ぶとObject
のequals()
が呼ばれます。
つまり、インスタンスの持つフィールドの一致を見るのではなく、
メモリ上で同じインスタンスを指す参照かどうかを比較します。
よって、HogeService
の中でnewした"太郎"
のStudent
と、
HogeServiceTest
の中でnewした"太郎"
のStudent
は別物と見做されるため、
前述の「expectedStudent
でreturnByStudent()
が呼ばれた回数」は「0回」です。
Stringやプリミティブ型だとeq()
で回数検証できますが、
複雑なクラスだとそうはいかないことがわかりました。
ArgumentCaptor
で引数を捕捉する
そうはいかないので終わりますというわけにもいかないので代替案です。
ArgumentCaptor
というクラスを使うと、
テスト対象クラスが他の処理に渡した引数を取得することができます。
指定方法は以下の通り。①②のどちらか片方です。
// 指定方法① テストクラスのフィールドでアノテーションを使う
@Captor
private ArgumentCaptor<Student> captor;
@Test
void テスト2() {
when(mogeService.returnByStudent(any())).thenReturn("太郎");
hogeService.execute();
// 指定方法② テストメソッド内でインスタンス生成
ArgumentCaptor<Student> captor = ArgumentCaptor.forClass(Student.class);
verify(mogeService, times(1)).returnByStudent(captor.capture());
}
このように書くことで、captor
に引数が貯め込まれるようなイメージです。
あとは実際の値を取得するだけです。
// 呼出1回目の引数を取得(何回呼んでも1回目のものしか取れない)
Student actual = captor.getValue();
// 複数回呼んでいるならgetAllValues()で全部取得する
List<Student> actualList = captor.getAllValues();
最終系は以下です。
取得さえできたらあとはassertするだけですね。
@Captor
private ArgumentCaptor<Student> captor;
@Test
void テスト2() {
when(mogeService.returnByStudent(any())).thenReturn("太郎");
hogeService.execute();
verify(mogeService, times(1)).returnByStudent(captor.capture());
Student actual = captor.getValue();
assertThat(actual.getName(), is("太郎"));
}
どんな型の引数でも使えはしますが、記述としては長くなるので
常に無差別でArgumentCaptor
を使うのではなく、
テストケース上eq()
やany()
で済む場合は正しく使い分けられるとグレートだと思います。