4
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

ひとりJUnitAdvent Calendar 2022

Day 8

JUnit + MockitoでMock化した処理の呼び出し回数と引数を検証する

Last updated at Posted at 2022-12-18

はじめに

ひとりJUnitアドベントカレンダー8日目の記事です。
前日に引き続きMockito関連です。

使用バージョン

pom.xml
<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とします。

HogeService.java
@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.");
  }
}
MogeService.java
@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;
  }
}

ご覧の通り、HogeServiceexecute()では
MogeServicereturnStr()returnNothing()を一度ずつ呼んでいます。
ただ、returnArg()は一度も呼んでいません。
これを検証します。

早めの結論、verifyを使う

サクッと結論を書くと、以下です。

HogeServiceTest.java
  @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()で一致することが前提です。

つまり、以下のようなケースは困ります。

HogeService.java
public class HogeService {

  public void execute() {
    Student student = new Student("太郎");
    mogeService.returnByStudent(student);
  }
}
MogeService.java
public class MogeService {

  public String returnByStudent(Student student) {
    return student.toString();
  }
}

HogeServiceMogeService#returnByStudent()を呼び出しています。
returnByStudent()Studentを引数に取るメソッドで、
HogeServiceは文字列"太郎"を元にStudentをnewして処理に渡すような形です。

HogeServiceTest.java
  @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()を呼ぶとObjectequals()が呼ばれます。

つまり、インスタンスの持つフィールドの一致を見るのではなく、
メモリ上で同じインスタンスを指す参照かどうかを比較します。

よって、HogeServiceの中でnewした"太郎"Studentと、
HogeServiceTestの中でnewした"太郎"Studentは別物と見做されるため、
前述の「expectedStudentreturnByStudent()が呼ばれた回数」は「0回」です。

Stringやプリミティブ型だとeq()で回数検証できますが、
複雑なクラスだとそうはいかないことがわかりました。

ArgumentCaptorで引数を捕捉する

そうはいかないので終わりますというわけにもいかないので代替案です。
ArgumentCaptorというクラスを使うと、
テスト対象クラスが他の処理に渡した引数を取得することができます。

指定方法は以下の通り。①②のどちらか片方です。

HogeServiceTest.java
  // 指定方法① テストクラスのフィールドでアノテーションを使う
  @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するだけですね。

HogeServiceTest.java
  @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()で済む場合は正しく使い分けられるとグレートだと思います。

4
3
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
4
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?