3
1

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 9

JUnit + Mockito小ネタ 呼出ごとにMockの挙動を変える/呼出回数の様々な検証方法

Posted at

はじめに

ひとりJUnitアドベントカレンダー9日目の記事です。
今回は二日前一日前の記事に入れ漏れた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

前提

以下のようなクラス構成を想定します。

HogeService.java
@Service
@RequiredArgsConstructor
public class HogeService {

  @NonNull
  private MogeService mogeService;

  public void execute() {
    System.out.println(mogeService.returnStr());
    System.out.println(mogeService.returnStr());
  }
}
MogeService.java
@Service
public class MogeService {

  public String returnStr() {
    return new Random().nextBoolean() ? "foo" : "bar";
  }
}

呼出ごとにMockの挙動を変える

HogeServiceMogeServicereturnStr()を二回呼んでいます。
テストの中で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();

今回想定するテスト対象は以下なので、上記テストは通ります。

HogeService#execute
  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))するのも馬鹿馬鹿しいですし、
せっかくライブラリ側が用意してくれているので、適切に使ってできるだけラクしたいですね。

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

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?