テスト原則
言語固有の参照
言語固有のテストパターンについては、以下も参照してください:
- TypeScript(Vitest/RTL/Playwright): references/typescript.md
コアテスト哲学
- テストは第一級のコードである: プロダクションコードと同等のテスト品質を維持する
- 迅速なフィードバック: テストは高速に実行され、即座にフィードバックを提供する
- 信頼性: テストは決定論的で再現可能でなければならない
- 独立性: 各テストは分離して実行されなければならない
TDDプロセス [すべてのコード変更に対して必須]
すべてのコード変更に対してこのプロセスを実行すること:
REDフェーズ
ステップ1: 期待される動作を定義するテストを書く ステップ2: テストを実行する ステップ3: テストが失敗することを確認する(パスした場合、テストが間違っている)
GREENフェーズ
ステップ1: テストをパスさせる最小限のコードを書く ステップ2: テストを実行する ステップ3: テストがパスすることを確認する
REFACTORフェーズ
ステップ1: コード品質を改善する(重複の排除、命名の改善) ステップ2: テストを実行する ステップ3: テストが依然としてパスすることを確認する
VERIFYフェーズ [必須 - エラー0必須]
ステップ1: 言語/プロジェクトのすべての品質チェックコマンドを実行する ステップ2: すべてのコマンドがエラー0でパスするまでエラーを修正する ステップ3: リグレッションがないことを確認する
強制: 品質チェックの失敗がある場合、次のフェーズに進むことはできない
TDD例外(TDD不要)
- 純粋な設定ファイル
- ドキュメントのみ
- 緊急修正(ただし、修正後すぐにテストを追加すること)
- 探索的スパイク(マージ前に破棄するか、テスト付きで書き直すこと)
- ビルド/デプロイスクリプト(ビジネスロジックを含む場合を除く)
品質要件 [必須]
カバレッジ
- カバレッジは「未テスト領域を見つけるための診断信号」として扱い、目標としては扱わない。目標にすると、些細なテストでごまかされる。
- テストは重要パス、ビジネスロジック、リグレッションが問題となる動作に集中させる。
- カバレッジ数よりも意味のあるアサーションを優先する。必要なしきい値は、プロジェクトのCI、タスクファイル、作業計画、またはDesign Docから取得する。
テストの特性
すべてのテストは以下の特性を満たさなければならない:
- 独立: テスト間の依存関係がない
- 再現可能: 同じ入力は常に同じ出力を生成する
- 高速: プロジェクトの許容フィードバックウィンドウ内で完全なテストスイートを完了し、ローカルでのイテレーションやCIを著しく遅くするスイートにフラグを立てる
- 自己検証: 手動検証なしで明確なパス/フェイル
- タイムリー: テスト対象のコードに近接して書かれている
強制: いずれかの特性を満たさないテストは直ちに修正されなければならない
テストの種類
単体テスト
目的: 個々のコンポーネントを分離してテストする
特徴:
- 単一の関数、メソッド、またはクラスをテストする
- 高速実行(ミリ秒)
- 外部依存関係なし
- 外部サービスはモックする
- テストスイートの大部分を占める
結合テスト
目的: コンポーネント間の相互作用をテストする
特徴:
- 複数のコンポーネントを一緒にテストする
- データベース、ファイルシステム、APIを含む場合がある
- 単体テストより遅い
- モジュール間の契約を検証する
- テストスイートの一部を占める
エンドツーエンド(E2E)テスト
目的: ユーザー視点から完全なワークフローをテストする
特徴:
- アプリケーションスタック全体をテストする
- 実際のユーザーインタラクションをシミュレートする
- 最も遅いテストタイプ
- 数が最も少ない
- 最も高い信頼レベル
テストピラミッド
テストピラミッド構造に従う:
/\ <- 少数のE2Eテスト(高信頼性、低速)
/ \
/ \ <- 一部の結合テスト(中程度の信頼性、中程度の速度)
/ \
/________\ <- 多数の単体テスト(高速、基盤)
テスト設計原則
AAAパターン(Arrange-Act-Assert)
すべてのテストを3つの明確なフェーズで構成する:
// Arrange: テストデータと条件をセットアップ
user = createTestUser()
validator = createValidator()
// Act: テスト対象のコードを実行
result = validator.validate(user)
// Assert: 期待される結果を検証
assert(result.isValid == true)
概念ごとに1つのアサーション
- テストケースごとに1つの動作をテストする
- 単一の概念をテストする場合は複数のアサーションも可
- 無関係なアサーションは別のテストに分割する — 1つのテストは1つの動作を検証しなければならない
説明的なテスト名
テスト名は以下を明確に説明する必要がある:
- 何をテストしているか
- どのような条件下で
- 期待される結果は何か
推奨フォーマット: 「[条件]の場合、[期待される動作]になること」
テストの独立性
分離要件
- 各テストは自身のテストデータを作成する
- 実行順序への依存関係なし
- 自身の状態をクリーンアップする
- 単独で実行してもパスする
セットアップとティアダウン
- セットアップフックを使用してテスト環境を準備する
- ティアダウンフックを使用してリソースをクリーンアップする
- セットアップは、テスト対象の動作に必要なデータ、依存関係、フィクスチャにスコープを限定する
- テストが失敗した場合でもティアダウンが実行されることを保証する
モックとテストダブル
モックを使用すべき場合
- 外部依存関係をモックする: API、データベース、ファイルシステム
- 低速な操作をモックする: ネットワークコール、重い計算
- 予測不能な動作をモックする: ランダム値、現在時刻
- 利用できないサービスをモックする: サードパーティサービス
モックの原則 [必須]
- 境界でモックし、内部ではモックしない — 内部ユーティリティには実際の実装を使用する
- 各モックは、テストが制御または観察する必要がある動作のみに限定する
- 該当する場合はモックの期待を検証する
- 制御できない外部ライブラリ/フレームワークにはアダプターを使用する
テストダブルの種類
- スタブ: 所定の値を返す
- モック: 正しく呼び出されたかを検証する
- スパイ: 呼び出しに関する情報を記録する
- フェイク: 簡略化された実際の実装
- ダミー: 渡されるが使用されない
データレイヤーテスト
モックの限界(データアクセス)
モックは呼び出しパターンを検証するが、スキーマの正確性、クエリの正確性、ストレージ制約を検証しない。 モックが見逃す可能性のある問題の例:
- スキーマのズレ
- カラムまたはフィールドの不一致
- 誤った結合、フィルター、または集計
- マイグレーションの非互換性
実際のデータレイヤー検証が価値を発揮する場合
以下のテストでは、実際の、またはプロダクションに近いデータアクセス検証を使用する:
- リポジトリまたはDAOの実装
- ORMマッピング
- クエリビルダーまたは生SQL
- 制約やスキーマ形状に依存する永続化動作
環境オプション
プロジェクト環境に最も実用的なオプションを選択する:
- コンテナ化されたデータベース
- 専用テストデータベース
- 制限が文書化されたインメモリデータベース
- アダプターベースのローカルテストハーネス
設計の整合性
Design Docに「テスト境界」が含まれている場合、それをベースラインとして、どの依存関係を実際のものにし、どの境界を分離するかを決定する。
テスト品質プラクティス [必須]
テストをアクティブに保つ
- 失敗するテストを修正または削除する: 失敗を直ちに解決する
- コメントアウトされたテストを削除する: 修正するか完全に削除する
- テストを実行し続ける: 壊れたテストはすぐに価値を失う
- テストスイートを維持する: 必要に応じてテストをリファクタリングする
テストコードの品質
- プロダクションコードと同じ基準を適用する
- 説明的な変数名を使用する
- テストヘルパーを抽出して重複を減らす
- テストを読みやすく、保守しやすく保つ
テストヘルパーとユーティリティ
- 再利用可能なテストデータビルダーを作成する
- 共通のセットアップをヘルパー関数に抽出する
- 複雑なシナリオ向けのテストユーティリティを構築する
- 適切にテストファイル間でヘルパーを共有する
テストすべき対象
動作に焦点を当てる
実装ではなく、観察可能な動作をテストする:
- 良い例:関数が期待される出力を返すことをテストする
- 良い例:正しいAPIエンドポイントが呼び出されることをテストする
- 悪い例:内部変数が設定されたことをテストする
- 悪い例:プライベートメソッドの呼び出し順序をテストする
エッジケースをテストする
常に以下をテストする:
- 境界条件: 最小値/最大値、空のコレクション
- エラーケース: 無効な入力、null値、欠落データ
- エッジケース: 特殊文字、極端な値
- ハッピーパス: 通常の期待される使用
テスト品質基準 [必須]
- リテラル期待値: アサーションにはハードコードされたリテラル値を使用する — 期待値 ≠ モックの戻り値(実装がデータを処理する)
- 結果検証: 戻り値と状態をアサーションし、呼び出し順序ではない
- 意味のあるアサーション: すべてのテストには少なくとも1つのアサーションが必須 — アサーションのないテストはゼロの価値しかない
- 外部I/Oのみをモックする: DB/API/ファイルシステムをモックし、内部ユーティリティは実際のものを使う
- 境界カバレッジ: ハッピーパスに加えて、空/ゼロ/最大/エラーケースを含める
強制: いずれかの基準に違反するテストは書き直されなければならない
検証要件 [VERIFYフェーズでは必須]
コミット前チェックリスト
☐ すべてのテストがパスしている ☐ スキップまたはコメントアウトされたテストがない ☐ テストにデバッグコードが残っていない ☐ プロジェクト、タスクファイル、作業計画、またはDesign Docで定義されている場合、カバレッジしきい値をパスしている ☐ テストが妥当な時間内に実行される
ゼロトレランスポリシー
- ゼロの失敗テスト: 直ちに修正する
- ゼロのスキップテスト: 削除するか修正する
- ゼロの不安定なテスト: 決定論的にする
- ゼロの遅いテスト: 最適化するか分割する
強制: 品質チェックの失敗がある場合、タスク完了に進むことはできない
テストの整理
ファイル構造
- プロダクション構造を反映: テストはコードの構成に従う
- 明確な命名規則: プロジェクトのテストファイルパターンに従う
- 論理的なグループ化: 関連するテストを一緒にグループ化する
- テストタイプの分離: 単体、結合、E2Eテストを別々のディレクトリに配置する
テストスイートの構成
tests/
├── unit/ # 高速で分離された単体テスト
├── integration/ # 結合テスト
├── e2e/ # エンドツーエンドテスト
├── fixtures/ # テストデータとフィクスチャ
└── helpers/ # 共有テストユーティリティ
パフォーマンスに関する考慮事項
テスト速度
- 単体テスト: 各 < 100ms
- 結合テスト: 各 < 1s
- 完全なスイート: 頻繁に実行可能であるべき(< 10分)
一般的なアンチパターン
これらのパターンを検出し、直ちに排除する:
- 何もテストしないテスト(常にパスする)
- 実行順序に依存するテスト
- 外部状態に依存するテスト
- 複雑なロジックを持つテスト(独自のテストが必要なテスト)
- 観察可能な動作ではなく実装の詳細をテストする
- 過剰なモック(境界のみモックし、内部は実際のものを使用する)
- テストコードの重複
不安定なテスト
断続的に失敗するテストを排除する:
- 時間依存性を削除する
- ランダム値ではなく決定論的なデータを使用する
- 適切なクリーンアップを確保する
- 競合状態を修正する
- すべてのテストを決定論的にする
リグレッションテスト
- すべてのバグ修正に対してテストを追加する
- 包括的なテストスイートを維持する
- 定期的に完全なスイートを実行する
- テストを削除するのは、カバーされていた動作が存在しなくなった場合、または同じ動作が適切なレベルでより強力なテストでカバーされている場合のみ
レガシーコード
- リファクタリング前に特性テストを追加する
- まず既存の動作をテストする
- 徐々にカバレッジを改善する
- 自信を持ってリファクタリングする