「ユニットテストの役割って、実装した時点でのコードの確かさを担保することですよね?」

その通りだ。でも、それだけではない。

テストには複数の役割がある。

  1. 実装時点での確かさ - コードが仕様通りに動くことを確認
  2. リグレッション防止 - 将来の変更で既存機能が壊れていないことを検出
  3. リファクタリングの安全網 - 内部実装を変更しても動作保証
  4. 仕様のドキュメント化 - テストコードが「何をすべきか」の生きた文書になる
  5. 設計へのフィードバック - テストしにくいコードは設計に問題がある

実装時点での確かさを作っておけば、他の箇所を修正したときにテストが失敗したら「何か変なことをした」と気づける。

テスト失敗の意味

「テスト失敗したら修正すればいいんでしょ?それって意味あるの?」

この質問は本質を突いている。「テスト失敗したら直せばいい」だけだと確かに意味が薄い。

ポイントはなぜ失敗したかを考えるところにある。

テストが失敗したとき、2つのケースがある。

ケース 原因 対処
意図しない変更 バグを入れた コードを直す(バグを防げた)
意図した仕様変更 仕様が変わった テストを更新する(影響範囲が可視化された)

テストがなければ、ケース1は本番で発覚するまで気づけない。ケース2は「この変更がどこに影響するか」を人間の記憶や勘で追うしかない。

つまりテストは「直すかどうか判断する機会」を与えてくれるもの。機械的に直すものではない。

AIにテスト実装を任せる

ここからが本題。AIエージェントにテスト実装を任せるのはアリか?

結論から言うと、全然アリ。むしろ相性がいい領域だ。

AIがテスト実装で得意なこと

  • 機械的なテストコード生成 - 仕様が明確なら、テストは比較的定型的
  • 網羅性の担保 - エッジケース、境界値の洗い出しはAIが得意
  • 既存コードからのテスト生成 - パターン認識で適切なテストを推測

AIがテスト実装で苦手なこと

  • 何をテストすべきかの判断 - ビジネス要件の理解が必要
  • 実装詳細への過度な依存 - 「このコードをそのままテストする」と、リファクタリング耐性がなくなる
  • テストの意味の保証 - 「テストが通る」ことと「正しい仕様を検証している」ことは別

実用的なアプローチ

人間とAIで役割分担する。

人間:「このケースをテストして」「公開APIの振る舞いを検証して」
       ↓ 方針を与える
AI:テスト実装を担当
       ↓ 生成結果
人間:レビューしてノイズになりそうなテストを削る

テスト実装の労力が減れば、「実装 → テスト → 変更 → 失敗分析」のサイクルを回す頻度を上げやすくなる。

テストの質をどう担保するか

AIが書いたテストが「ちゃんと検証しているか」をどう判断するか?

1. Mutation Testing

コードの一部をわざと壊して、テストが失敗するかを確認する。

// 元のコード
function isAdult(age: number): boolean {
  return age >= 18;
}

// ミュータント1: >= を > に変更
function isAdult(age: number): boolean {
  return age > 18;  // 18歳がfalseになる
}

// ミュータント2: 18 を 17 に変更
function isAdult(age: number): boolean {
  return age >= 17;  // 17歳がtrueになる
}

良いテストはこれらのミュータントを「殺す」(失敗させる)。殺せないミュータントが多いなら、テストが弱い証拠だ。

# Strykerで実行
npx stryker run
# Mutation Score: 85% (100体中85体のミュータントを殺した)

2. Test Validity Audit

AIに「このテストは意味のある検証をしているか?」を批判的に評価させる。

ただし、実装したAIと監査するAIは分ける。自分で書いたテストを自分で評価すると甘くなる。

audit_agent:
  isolation:
    mode: separate_session  # 必須
    model: separate_model   # 推奨(実装がSonnetなら監査はOpus)
    context_sharing: none   # 成果物のみ共有

3. Negative Case Ratio

異常系・エッジケースのテストが全体の何割を占めるかを測定する。

全テスト: 100件
  - 正常系: 70件
  - 異常系: 30件
→ Negative Case Ratio: 30%

ハッピーパスばかりのテストは危険。現実世界では異常系のほうが多く発生する。

実践的なディレクトリ構成

テスタブルなアーキテクチャを最初から設計する。

src/
├── domain/           # 純粋なビジネスロジック(外部依存なし)
│   ├── entities/
│   ├── value-objects/
│   └── services/
├── application/      # ユースケース層
│   ├── use-cases/
│   └── ports/        # インターフェース定義
├── infrastructure/   # 外部接続(DB、API)
│   └── adapters/     # Portの実装
└── presentation/     # UI層
    └── components/

tests/
├── unit/             # 単体テスト(domain層メイン)
├── integration/      # 結合テスト(use-case + adapter)
└── e2e/              # エンドツーエンド

ポイント

  • Domain層は純粋関数 - 外部依存なしでテストしやすい
  • Application層はPort経由 - モック注入でテスト可能
  • Infrastructure層は薄く - ロジックを持たせない

AIテスト委譲のチェックリスト

AIにテスト実装を任せる際の確認事項。

  • テストすべき対象(公開API / 内部実装)を人間が指定したか
  • 生成されたテストをレビューして、実装詳細への依存を排除したか
  • Mutation Scoreを計測して、テストの検知能力を確認したか
  • 異常系のテストが適切な割合で含まれているか
  • テストが失敗したとき、その理由を分析するプロセスがあるか

まとめ

AI時代のテスト戦略は、「テストを書く」作業をAIに任せつつ、「何をテストすべきか」「テストの質は十分か」を人間が判断することにシフトする。

テスト実装の労力が減ることで、テストを書くハードルが下がり、結果的にコードベース全体の品質が上がる。これがAI活用の本当のメリットだ。