テストは「将来壊してはいけないもの」を書き残すもの — LaravelのFeature Test中心の考え方
この記事で扱う問題
テストを書く目的が「カバレッジを上げる」になると、次のようなテストが増えます。
- getter を呼んで値を確かめるだけのテスト
- 実装の手順をそのままなぞり、モックの呼び出し回数を確かめるテスト
こうしたテストは、実装を少し変えるだけで壊れます。一方で、本当に壊してはいけない振る舞い(二重送信で二重課金しない、他人のデータを見られない)は、テストされていないことがあります。
判断
私はテストを、将来の変更で何を壊してはいけないかを書き残すものとして扱います。
Laravel では、Feature Test(HTTP リクエストから DB までを通すテスト)を中心にします。
public function test_他人の注文は確定できない(): void
{
$owner = User::factory()->create();
$other = User::factory()->create();
$order = Order::factory()->for($owner)->pending()->create();
$this->actingAs($other)
->postJson("/api/orders/{$order->id}/confirm")
->assertForbidden();
$this->assertSame('pending', $order->fresh()->status);
}
そのうえで、条件の組み合わせが多い業務ルール(料金計算、状態遷移など)には、Unit Test を足します。
重点的に書くのは次の観点です。
- 正常系(代表的な1ケースで十分)
- 境界値(0件、上限ちょうど、日付の変わり目)
- 異常系(入力不正、存在しないデータ、状態が合わない)
- 外部サービスの障害(タイムアウト、エラー応答)
- DB の整合性(失敗したときに途中の状態が残らない)
- 冪等性(同じリクエストを2回送っても結果が変わらない)
- 認証・認可(他人のデータに触れない)
なぜそう判断したか
Feature Test を中心にするのは、利用者から見た振る舞いを守りたいからです。内部の実装を入れ替えても、振る舞いが同じならテストは通り続けます。リファクタリングの安全網として働くのは、このタイプのテストです。
一方で、Feature Test だけで料金計算の全組み合わせを確かめると、遅くて読みにくくなります。組み合わせの多いルールは、DB に依存しないドメインのクラスに切り出し、Unit Test で網羅します。
テスト名を日本語で「他人の注文は確定できない」のように書くのは、テストの一覧がそのまま「壊してはいけないものの一覧」として読めるようにするためです。
どこから設計過剰になるか
- 単純な CRUD で、正常系・異常系を細かく書き分けても得るものは少ないです。認可と入力検証に絞れば足ります
- 捨てる前提の PoC では、テストより「早く作って失敗条件を確かめる」ことを優先します
- モックを多用して実装の手順を検証するテストは、壊れやすさに見合う価値があるか一度疑うべきです
まとめ
- テストの目的は数ではなく、将来の変更で壊してはいけないものを書き残すこと
- Laravel では Feature Test を中心にし、組み合わせの多いルールだけ Unit Test で網羅する
- 境界値・異常系・外部障害・整合性・冪等性・認可を重点的に書く