テスト駆動開発
哲学
中核原則: テストは実装の詳細ではなく、公開インターフェースを通じて振る舞いを検証すべきです。コードは完全に変更される可能性があります。テストは変更されるべきではありません。
良いテストは統合スタイルです。公開APIを通じて実際のコードパスを実行します。システムが どのように 動作するかではなく、 何を 行うかを記述します。良いテストは仕様書のように読めます。「ユーザーは有効なカートでチェックアウトできる」は、どのような機能が存在するかを正確に伝えます。これらのテストは内部構造を気にしないため、リファクタリング後も有効です。
悪いテストは実装に結合しています。内部の協力者をモックし、プライベートメソッドをテストし、外部手段(データベースに直接問い合わせるなど)で検証します。危険信号:振る舞いが変わっていないのに、リファクタリングでテストが壊れる。内部関数をリネームするとテストが失敗する場合、それらのテストは実装をテストしており、振る舞いをテストしていません。
例とガイドラインについては、tests.md および mocking.md を参照してください。
アンチパターン:水平スライス
すべてのテストを先に書き、その後すべての実装を行うのは避けてください。 これは「水平スライス」です。REDフェーズを「全テストを書く」、GREENフェーズを「全コードを書く」と扱います。
これは質の悪いテストを生み出します:
- 一括で書かれたテストは、実際の 振る舞いではなく、想定された 振る舞いをテストする。
- 結局、ユーザー向けの振る舞いではなく、ものの 形(データ構造、関数シグネチャ)をテストすることになる。
- テストは実際の変更に鈍感になる——振る舞いが壊れてもテストがパスし、振る舞いが正しくてもテストが失敗する。
- 実装を理解する前にテスト構造にコミットし、予測不能な領域に踏み込んでしまう。
正しいアプローチ: トレーサー弾による垂直スライス。1テスト→1実装→繰り返し。各テストは前のサイクルから学んだことに基づいて書かれます。コードを書いたばかりなので、どの振る舞いが重要で、それをどう検証するかが正確にわかります。
間違い(水平):
RED: test1, test2, test3, test4, test5
GREEN: impl1, impl2, impl3, impl4, impl5
正しい(垂直):
RED→GREEN: test1→impl1
RED→GREEN: test2→impl2
RED→GREEN: test3→impl3
...
ワークフロー
1. 計画
コードを書き始める前に:
- ユーザーと必要なインターフェース変更を確認する
- ユーザーとテスト対象の振る舞いを確認する(優先順位付け)
- 深いモジュール(小さなインターフェース、深い実装)の機会を特定する
- テスト容易性のためにインターフェースを設計する
- テスト対象の振る舞いをリストアップする(実装手順ではない)
- 計画についてユーザーの承認を得る
質問:「公開インターフェースはどのようにあるべきですか? どの振る舞いを最も重点的にテストすべきですか?」
すべてをテストすることはできません。 ユーザーと正確にどの振る舞いが最も重要かを確認してください。すべてのエッジケースではなく、重要なパスと複雑なロジックにテスト労力を集中させましょう。
2. トレーサー弾
システムについて1つのことを確認する1つのテストを書きます:
RED: 最初の振る舞いのテストを書く → テスト失敗
GREEN: テストを通す最小限のコードを書く → テスト成功
これがトレーサー弾です——パスがエンドツーエンドで機能することを証明します。
3. インクリメンタルループ
残りの各振る舞いに対して:
RED: 次のテストを書く → 失敗
GREEN: 通すための最小限のコード → 成功
ルール:
- 一度に1つのテスト
- 現在のテストを通すのに十分なコードのみ
- 将来のテストを先取りしない
- テストは観察可能な振る舞いに焦点を当てる
4. リファクタリング
すべてのテストが通った後、リファクタリング候補を探す:
- 重複を抽出する
- モジュールを深くする(複雑さをシンプルなインターフェースの背後に移動する)
- 自然な場所でSOLID原則を適用する
- 新しいコードが既存のコードについて何を明らかにしているかを検討する
- リファクタリングステップごとにテストを実行する
REDの状態ではリファクタリングしないでください。 まずGREENに到達しましょう。
サイクルごとのチェックリスト
[ ] テストは実装ではなく振る舞いを記述している
[ ] テストは公開インターフェースのみを使用している
[ ] テストは内部リファクタリング後も有効である
[ ] コードはこのテストに対して最小限である
[ ] 推測による機能追加は行っていない