モックで依存を差し替える
このレッスンでは、テストのために依存を偽物へ差し替える手口を整理し、呼び出し回数を数える偽実装を自分で書けるようになります。
偽物には3つの呼び名があります
テスト用の偽物は、役割によって呼び分けられています。境目は厳密ではありませんが、狙いが違います。
| 呼び名 | 役割 | 主に確かめること |
|---|---|---|
| スタブ (stub) | 決まった値を返すだけ | 戻り値を受けた側の振る舞い |
| モック (mock) | 呼ばれ方を検証する | 正しく呼ばれたかどうか |
| スパイ (spy) | 記録を取りつつ動く | 呼び出し回数や引数 |
スタブは「このメソッドを呼んだらこの値を返す」とだけ決めておく置物です。データベースから見つからなかった場合を再現したいときなどに使います。モックは逆に、戻り値より呼ばれたという事実のほうが関心事です。メールを送る処理なら、送信そのものはテストで実行したくないけれど、1回だけ呼ばれたことは確かめたい、といった場面です。スパイはその中間で、記録を取りながら実際の処理も動かします。
Mockito の2つの動詞
Java では Mockito というライブラリがこの役目を担います。使う命令はほぼ2つに集約されます。
Java
// 準備 — この呼び出しにはこの値を返させる
when(userRepository.findById(1L)).thenReturn(Optional.of(user));
// 検証 — この呼び出しがちょうど1回起きたことを確かめる
verify(mailSender, times(1)).send("alice@example.com");when と thenReturn の組がスタブの宣言です。verify が呼ばれ方の検証で、times(1) のほか never() や atLeastOnce() を渡せます。値を返さない void のメソッドは when で包めないので、こういうメソッドこそ verify の出番になります。
@MockBean はコンテナの中身を置き換えます
ここまでは Spring と無関係です。new して渡すだけなら、モックはただのオブジェクトだからです。一方、@WebMvcTest や @SpringBootTest のようにコンテナが起動するテストでは、コンテナが持っている Bean を差し替える必要があります。それをやるのが @MockBean です。
Java
@WebMvcTest(UserController.class)
class UserControllerTest {
@Autowired MockMvc mockMvc;
@MockBean UserService userService; // コンテナ内の UserService をモックに置換
}@MockBean を書くと、同じ型の Bean がコンテナにあれば置き換え、なければ追加します。コントローラに注入されるのはこのモックになります。
混同しやすいのは @Mock との違いです。@Mock は Mockito 単体のアノテーションで、コンテナには一切触れません。Spring を起動しない単体テストで使うものです。逆に、Spring を起動しないテストで @MockBean を書いても意味がありません。差し替える先のコンテナが存在しないからです。コンテナが立っているかどうかが使い分けの分かれ目になります。
モックを使いすぎない
最後に注意点をひとつ挙げます。モックは、対象が本当にそう振る舞うかではなく、自分が決めた振る舞いを返します。すべてをモックで固めたテストは、実装を書き換えても常に緑のまま通ってしまう可能性があります。差し替えるのは、遅いもの、外部にあるもの、再現しづらいものに留めるのが健全です。純粋な計算は本物のまま呼びます。
演習で書くこと
Mockito が裏でやっていることを自分の手で作ります。メール送信のインターフェースに対して、呼ばれた回数を数える偽の実装を書き、それを Service に渡して動かします。最後に verify に相当する確認として、呼び出し回数と処理結果をまとめて返します。
要件
- SpyMailSender の send は calls を 1 増やしてから、コンストラクタで受け取った result を返すこと
- notifyAll は csv をカンマで分解し、空文字列の宛先を読み飛ばしたうえで send を呼び、true が返った件数を返すこと
- verify は calls=2,ok=2 のように、呼び出し回数と成功件数をつないだ文字列を返すこと
入出力例
verify("a@example.com,b@example.com", true) → "calls=2,ok=2"
verify("a@example.com,b@example.com,c@example.com", false) → "calls=3,ok=0"
verify("solo@example.com", true) → "calls=1,ok=1"
verify("", true) → "calls=0,ok=0"
verify("a@example.com,,b@example.com", true) → "calls=2,ok=2"