はじめに
ひとりJUnitアドベントカレンダー10日目の記事です。
端的に言おうとしすぎてタイトルが意味不明な文章になってる。
使用バージョン
<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書かざるを得ない場面を想定します。
@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;
}
}
@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
からHogeService
にStudent
インスタンスを返却せずとも
HogeService
では内容変更後のインスタンスを扱うことができます。
ただ、HogeService
上ではMogeService
側でどんな変更が行われるかわかりませんし
不完全な状態のStudent
が一時的に生まれるという点でリスクもあるため、微妙です。
そもそもこのコードであればMogeService
側でStudent
を生成してreturnする方が
よっぽど綺麗ですが、まあ更に別の処理があったり複雑な事情があったりするのでしょう。
これのテストコードを書いてみます。
とりあえず書けはしたものの
Mockitoを使ってMogeService
をmock化してテストしますが、
MogeService#update()
はvoidなので特に挙動を指定せずに書いてみます。
@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
です。
@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
インターフェースはメソッドを一つしか持たないため、ラムダ式で書けます。
@Test
void テスト() {
doAnswer(invocation -> {
invocation.getArgument(0, Student.class).setName("太郎");
return null;
}).when(mogeService).update(any());
int actual = hogeService.execute();
assertThat(actual, is(1));
}
不慣れな場合は焼け石に水というかむしろ難解かもですが・・・
再利用のタイミングが多いのであればクラスとして切ってしまうのもアリだと思います。
@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
のご紹介でした。