Test Philosophy
ユニットテストの設計指針。テストの種類に応じた書き方のルールと、保守性の高いテストを書くための構造的なガイドラインを定義する。
テストの 3 分類
1. Exploratory Test (探索的テスト)
開発中に動作を素早く確認するためのテスト。REPL のように使う。
- •実装の理解を深めるために積極的に書く
- •現実的な入力データを使ってよい
- •テスト名に
(exploratory)を付ける - •コミット対象外 - 開発中のみ存在する一時的なテスト
2. Protective Test (保護テスト)
リグレッション防止のためにリポジトリに残すテスト。
- •長期的に維持するテスト
- •テストケースの選定 (何をテストするか): 実装の知識を使って重要なケースを選ぶ (ホワイトボックス的)
- •テストの書き方 (どうテストするか): 公開インターフェースのみを通じてテストする (ブラックボックス的)
詳細は Protective Test ガイドライン を参照。
3. Temporary Refactoring Test (一時的リファクタリングテスト)
特定のリファクタリング中に動作を保護するためのテスト。
- •リポジトリにコミットするが、リファクタリング完了後に削除する
- •保守性よりカバレッジを優先してよい (内部 API のテストも許容)
- •テスト名に
(refactoring)を付ける - •削除条件を必ずコメントで記述する (例:
// Remove after: migration from X to Y is complete)
Protective Test ガイドライン
テストケース選定の原則
テストは多ければ良いわけではない。バランスが重要。
- •直交する軸を網羅的に組み合わせない - 実装上独立している軸なら、全組み合わせは不要
- •実装のミラーテストを書かない - 定数テーブルの再検証のような、実装をそのままなぞるテストは避ける
- •ハッピーパスを優先する - エラーパスも大事だが、正常系のカバレッジが先
- •起こりそうにないバグを先回りしない - 予測可能なバグパターン以外への投機的防御は避ける
- •リグレッションテストを追加しやすい状態を保つ - バグ発見時にすぐテストを足せるようにする
テスト構造: 4 フェーズ
- •Setup - テストコンテキストの構築
- •Exercise - テスト対象の公開インターフェースを呼び出す
- •Verify - 契約 (contract) を検証する
- •Teardown - クリーンアップ
1 テスト = 1 契約
- •各テストは実装が満たすべき 1 つの契約を検証する
- •複数の契約を検証している場合はテストを分割する
- •異なる契約がたまたま同じアサーションになる場合、重複は許容する
テスト名
テスト名は検証する契約を現在形で記述する。
- •
describeブロック内で主語が自明な場合:it("returns null when not found")を使う - •主語が必要な場合:
test("empty input produces only EOF")を使う - •テスト名が他のテスト名の prefix にならないようにする (名前ベースのフィルタリングのため)
Setup の設計
- •フィクスチャは契約の検証に必要な最小限の条件を表現する
- •現実のデータの忠実さよりシンプルさを優先する
- •無関係な詳細は隠し、関連する値は公開する - ファクトリ関数でデフォルト値の裏に無関係な部分を隠す
- •テスト間の独立性を DRY より優先する
テストダブルの使い分け
- •グローバルな状態ではなく、明示的なインジェクションを使う
- •ストレージ系の依存 (write してから read するもの): fake を使う (実物のように振る舞う動的なオブジェクト)
- •その他の依存: mock を使う (振る舞いを静的に指定するオブジェクト)
アサーション
- •すべてのアサーションが確実に実行されるようにする
- •等値アサーションを優先する - 無関係なフィールドのサニタイズや非決定性の排除 (乱数シード固定、時刻固定) で実現する
- •複数アサーションはコードスメル - 複数の契約を含んでいる可能性を示唆する
テストヘルパー
- •テストの意図を明確にするヘルパーの作成は推奨する
- •ただし、問題が実装のインターフェースにある場合は、ヘルパーで回避するのではなくインターフェースを修正する
- •テストヘルパー自体もテストする