2
0

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 10

JUnit + MockitoでMock化した処理の中でもう一つ何か処理を行わせる

Last updated at Posted at 2022-12-20

はじめに

ひとりJUnitアドベントカレンダー10日目の記事です。
端的に言おうとしすぎてタイトルが意味不明な文章になってる。

使用バージョン

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

こんなことはないと思いますが

テスタビリティ低いし違和感あるので思いとどまるべきコードなんだろうなと思いつつも、
書いてしまったor書かざるを得ない場面を想定します。

HogeService
@Service
@RequiredArgsConstructor
public class HogeService {

  @NonNull
  private MogeService mogeService;

  public int execute() {
    Student student = new Student();
    // studentの内容を更新
    mogeService.update(student);

    if ("太郎".equals(student.getName())) {
      return 1;
    }
    return 0;
  }
}
MogeService
@Service
public class MogeService {

  public void update(Student student) {
    if (new Random().nextBoolean()) {
      student.setName("太郎");
    } else {
      student.setName("次郎");
    }
  }
}

HogeServiceの中でStudentインスタンスを生成し、MogeServiceに渡すと
MogeService側でインスタンスの内容が変更され、
その後は更新したStudentインスタンスのメンバの値を元に分岐を行う処理です。

言うまでもないですが、MogeServiceにはStudentインスタンスの参照が渡されるので
MogeServiceからHogeServiceStudentインスタンスを返却せずとも
HogeServiceでは内容変更後のインスタンスを扱うことができます。

ただ、HogeService上ではMogeService側でどんな変更が行われるかわかりませんし
不完全な状態のStudentが一時的に生まれるという点でリスクもあるため、微妙です。
そもそもこのコードであればMogeService側でStudentを生成してreturnする方が
よっぽど綺麗ですが、まあ更に別の処理があったり複雑な事情があったりするのでしょう。

これのテストコードを書いてみます。

とりあえず書けはしたものの

Mockitoを使ってMogeServiceをmock化してテストしますが、
MogeService#update()はvoidなので特に挙動を指定せずに書いてみます。

HogeServiceTest
@ExtendWith(MockitoExtension.class)
class HogeServiceTest {

  @InjectMocks
  private HogeService hogeService;

  @Mock
  private MogeService mogeService;

  @Test
  void テスト() {
    int actual = hogeService.execute();
    assertThat(actual, is(0));
  }
}

とりあえず1ケースは書けました。
MogeService#update()はMock化した後に特に挙動を指定していないため、
本当のメソッドでは"太郎"か"次郎"のいずれかがsetされるはずですが、
今回のテストでは特に何もsetされず後続処理に進みます。

本記事が載るアドベントカレンダーの前日までの記事で、Mockitoの使い方を見てきましたが
触れたのは「特定の戻り値を返す」「例外を投げる」という挙動の設定方法のみでした。

HogeServiceを分岐網羅するテストを書くためには、
MogeService#execute()が"太郎"をsetしてくれないといけません。
しかし当該処理はvoidなので、今までの設定方法ではうまくいきません。
さあどうすればよいでしょうか。

答え:Answer

激寒ギャグになってしまいましたが、答えはAnswerです。

HogeServiceTest.java
  @Test
  void テスト() {
    doAnswer(new Answer() {
      @Override
      public Object answer(InvocationOnMock invocation) {
        invocation.getArgument(0, Student.class).setName("太郎");
        return null;
      }
    }).when(mogeService).update(any());

    int actual = hogeService.execute();
    assertThat(actual, is(1));
  }

doReturn(), doThrow()に続きdoAnswer()です。
要するにmock化されたメソッドの挙動を自分で自由に定義しちゃおうよというもので、
今回の場合はmock化されたメソッドの第一引数を取得して、"太郎"をsetしてみました。

ゴチャゴチャしていてわかりにくいですが、
Answerインターフェースはメソッドを一つしか持たないため、ラムダ式で書けます。

HogeServiceTest.java
  @Test
  void テスト() {
    doAnswer(invocation -> {
      invocation.getArgument(0, Student.class).setName("太郎");
      return null;
    }).when(mogeService).update(any());

    int actual = hogeService.execute();
    assertThat(actual, is(1));
  }

不慣れな場合は焼け石に水というかむしろ難解かもですが・・・

再利用のタイミングが多いのであればクラスとして切ってしまうのもアリだと思います。

HogeServiceTest.java
  @AllArgsConstructor
  private class SetNameAnswer implements Answer {

    private String name;

    @Override
    public Object answer(InvocationOnMock invocation) throws Throwable {
      invocation.getArgument(0, Student.class).setName(name);
      return null;
    }
  }

  @Test
  void テスト() {
    doAnswer(new SetNameAnswer("太郎")).when(mogeService).update(any());

    int actual = hogeService.execute();
    assertThat(actual, is(1));
  }
}

テスト側の処理はこれが一番シンプルですね。

本当に使えるのかこれ

一応何回か実プロジェクトで(それはむしろ意識的に)使っています、が
多くの場合、まずは実装をテスト容易な形にできないか検討してみても損はないと思います。

一応前述のケース以外の使い道としては、
引数の値に応じてreturnする処理を変えるためという目的も考えられなくはないですが、
テストの中にロジックを持たせすぎるとそのロジックが誤っていた場合に死にます。
thenReturn()などをチェーンさせていくことで1回目、2回目と挙動を指定できるので
Anwer内で引数を取得して分岐させるよりも、素直に複数回挙動設定しましょう。

ということで、使えるともしかしたら役に立つ場面が稀にあるかもしれないですが
基本的には使う必要がない(はずの)、Answerのご紹介でした。

2
0
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
2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?