Serviceの単体テスト
このレッスンでは、Service の業務ロジックを Spring を起動せずに検証する書き方を身につけます。
Service の単体テストに Spring は要りません
前のレッスンで見たとおり、最も速いテストは Spring をまったく起動しないテストです。Service が持っている値引き計算や状態判定のようなロジックは、DI コンテナがあってもなくても同じ結果を返します。つまり確かめるのにコンテナは不要です。
Java
class PriceServiceTest {
@Test
void 千円以上は1割引になる() {
PriceService service = new PriceService(); // ただの new
assertEquals(900, service.discount(1000));
}
}テストクラスに @SpringBootTest は付いていません。@Autowired もありません。ただ new して呼ぶだけです。これで数百件書いても数秒で終わります。
依存があるときは偽物を渡します
Service が Repository を持っている場合はどうするか。ここで効いてくるのが、このコースの序盤で扱ったコンストラクタインジェクションです。依存をコンストラクタで受け取る設計にしておけば、テストからは好きなものを渡せます。
Java
@Service
class UserService {
private final UserRepository repository;
UserService(UserRepository repository) { // ここが差し替え口になる
this.repository = repository;
}
}テストでは本物のデータベースにつながる UserRepository ではなく、決まった値を返すだけの偽物を渡します。データベースの用意も後片付けも不要になり、しかも「見つからなかったとき」のような再現しづらい状況を自由に作れます。フィールドインジェクションではこの差し替えができないので、コンストラクタインジェクションが推奨されるという話は、テストの側から見ると特にはっきりします。偽物の作り方そのものは次のレッスンで詳しく扱います。
JUnit 5 の基本
テストメソッドには @Test を付けます。判定には Assertions のメソッドを使います。よく使うものは次の通りです。
| メソッド | 何を確かめるか |
|---|---|
assertEquals(expected, actual) | 値が等しいこと |
assertTrue(condition) | 条件が真であること |
assertNull(value) | null であること |
assertThrows(型, 実行) | 指定した例外が投げられること |
引数の順番は期待値が先、実際の値が後です。逆に書いても多くの場合テストは通ってしまいますが、失敗したときのメッセージが期待値と実測値を取り違えた形で出るので読み違えます。
AAA という組み立て方
テストメソッドの中身は、3つの段に分けて書くと読みやすくなります。準備、実行、検証の3段で、頭文字を取って AAA と呼びます。
Java
@Test
void 会員は追加で5パーセント引かれる() {
// 準備 (Arrange)
PriceService service = new PriceService();
// 実行 (Act)
int actual = service.discount(1000, true);
// 検証 (Assert)
assertEquals(850, actual);
}実行の行が1行だけになっていることが大事です。ここが何行にもなっているテストは、一度に複数のことを確かめようとしている合図です。失敗したときにどこが壊れたのか分からなくなるので、テストを分けます。
何を確かめるべきか
単体テストで狙うべきは分岐と境界です。値引きの例なら、1000円未満、ちょうど1000円、1000円を超える、といった境目を並べます。バグは条件の境目に住み着くので、真ん中の値だけを確かめても意味が薄いのです。
演習で書くこと
値引き計算という業務ロジックと、それを確かめる assertEquals に相当する判定を自分で書きます。そして複数の入力に対して期待値と一致したかどうかを pass と fail の並びとしてまとめて返します。JUnit が裏でやっている「呼んで、比べて、結果を記録する」という流れを、自分の手で組み立てることになります。
要件
- discount は価格が負なら -1 を返し、10000 以上で 20、1000 以上で 10、それ未満で 0 の割引率を使い、member が true ならその割引率に 5 を足すこと
- 値引き後の価格は price - price * rate / 100 で計算し、int の演算のままにすること
- runTests は 2 件分の判定結果を pass または fail としてカンマでつないだ文字列を返すこと
入出力例
runTests(1000, false, 900, 10000, false, 8000) → "pass,pass"
runTests(500, false, 500, 1000, true, 850) → "pass,pass"
runTests(-1, false, -1, 10000, true, 7500) → "pass,pass"
runTests(1000, false, 999, 999, false, 999) → "fail,pass"