はじめに
PSSLの佐々木です。
AIコーディングアシスタントの進化により、テストコードの自動生成が身近になりました。しかし、ここに大きな落とし穴があります。
AIが実装コードを見ながらテストを書くと、実装のロジックをそのままテストにコピーしてしまうので仕様の不具合に気づくことができません。
これを「トートロジカルテスト(同義反復テスト)」と呼びます。
// 実装コード
function calculateTax(price: number): number {
return Math.floor(price * 0.1);
}
// AIが生成した"テスト"(実装を見て書いた場合)
it('税額を計算する', () => {
const price = 1000;
const expected = Math.floor(price * 0.1); // 実装と同じロジック!
expect(calculateTax(price)).toBe(expected);
});
このテストは常に成功します。なぜなら、実装が間違っていてもテストも同じ間違いをするからです。カバレッジは100%でも、バグは検出できません。
解決策:AIに実装コードを「見せない」
この問題を解決するため、仕様書のみを参照してテストを生成するワークフローを構築しました。
Claude Codeのエージェント機能を使い、テスト生成エージェントにはReadツールを与えないという技術的制約を設けます。
# .claude/agents/test-generator.md
---
name: test-generator
description: 仕様書のみに基づいてテストを生成する
tools: Write, Bash # Readがない = 実装コードを読めない
---
これにより、AIは仕様書(spec.md)だけを頼りにテストを書くしかなくなります。
アーキテクチャ全体像
specs/features/{feature}/
├── spec.md # 仕様書(Given/When/Then形式)
└── plan.md # 実装計画
↓ 並列実行
┌─────────────────┐ ┌─────────────────┐
│ implementer │ │ test-generator │
│ (tools: 全て) │ │ (tools: Write, │
│ │ │ Bash) │
│ 実装コードを │ │ 仕様書のみ参照 │
│ 自由に読み書き │ │ src/は読めない │
└────────┬────────┘ └────────┬────────┘
│ │
▼ ▼
src/lib/*.ts src/lib/*.test.ts
src/app/**/* e2e/*.spec.ts
仕様書のフォーマット
テスト生成の精度を上げるため、仕様書はGiven/When/Then形式で記述します。
## 受け入れ条件
### AC-01: 定額割引の適用
- Given: 価格1000円の商品がある
- When: クーポンコード「SAVE100」を適用する
- Then: 割引後価格は900円になる
### AC-02: パーセント割引の適用
- Given: 価格2000円の商品がある
- When: クーポンコード「OFF10」(10%割引)を適用する
- Then: 割引後価格は1800円になる
この形式により、AIはテストケースを正確に生成できます。
核心となるルール:仕様整合ルール
テストが失敗した場合、どちらを修正するかという判断が重要です。
私たちは「仕様整合ルール」を採用しました:
テストが失敗し、かつテストが仕様書と整合している場合、 実装を修正する(テストを修正しない)
# .claude/rules/src-lib.md
## 仕様整合ルール(Spec-aligned Rule)
テスト失敗時の対応:
1. テストが仕様書(spec.md)と整合しているか確認
2. 整合している → 実装を修正
3. 整合していない → テストを修正
これにより、仕様書が「信頼できる唯一の情報源(Single Source of Truth)」として機能します。
実際のワークフロー
Step 1: 仕様書を作成
# specs/features/discount-calculator/spec.md
## 概要
クーポンコードを適用して割引価格を計算する機能
## 受け入れ条件
### AC-01: 定額割引
- Given: 価格1000円
- When: SAVE100を適用
- Then: 割引後900円
### AC-04: 無効なクーポン
- Given: 価格1000円
- When: INVALIDを適用
- Then: エラー「無効なクーポンコードです」
Step 2: 並列エージェントを実行
# 2つのエージェントを同時に起動
claude --agent implementer "specs/features/discount-calculator/plan.mdに基づいて実装"
claude --agent test-generator "specs/features/discount-calculator/spec.mdに基づいてテスト生成"
Step 3: テストを実行して結果を確認
npm run test:run
npx playwright test
Step 4: 失敗があれば仕様整合ルールに従って修正
実際のプロジェクトでは、以下のような失敗がありました:
FAIL src/lib/discount.test.ts
✕ SAVE100で100円割引が適用される
Expected: 100
Received: 0
テストは仕様書通り「100円割引」を期待しています。実装側のバグなので、実装を修正しました。
GitHub Actions での自動化
CI/CDパイプラインで品質を担保します:
# .github/workflows/ci.yml
jobs:
test:
steps:
- name: Type check
run: npx tsc --noEmit
- name: Run unit tests
run: npm run test:run
- name: Run unit tests with coverage
run: npm run test:coverage
e2e:
steps:
- name: Run E2E tests
run: npx playwright test
さらに、ミューテーションテストで「テストの質」も検証します:
# .github/workflows/mutation.yml
- name: Run mutation tests
run: npm run test:mutation # Stryker
技術スタック
| 用途 | ツール |
|---|---|
| フレームワーク | Next.js 15 (App Router) |
| 単体テスト | Vitest |
| E2Eテスト | Playwright |
| ミューテーションテスト | Stryker |
| AIエージェント | Claude Code |
得られた効果
1. テストが仕様の検証になる
実装を見ずに書かれたテストは、「仕様通りに動くか」を純粋に検証します。
2. バグが確実に検出される
実際に3件の実装バグがテストで検出されました:
- 割引額の計算ミス
- クーポン情報の返却漏れ
- 0円時の割引処理
3. リファクタリングが安全になる
テストが仕様に基づいているため、内部実装を変更しても「振る舞いが変わっていないこと」を確認できます。
注意点とトレードオフ
仕様書のメンテナンスコスト
仕様書が古くなると、テストと実装が乖離します。仕様変更時は必ず spec.md を更新する運用が必要です。
E2Eテストのセレクター問題
仕様書だけではDOM構造がわからないため、E2Eテストでは data-testid の命名規則を事前に決めておく必要があります。
# 仕様書に記載
## UI要素のTestID
- 価格入力: `data-testid="price-input"`
- 計算ボタン: `data-testid="calculate-button"`
- 結果表示: `data-testid="discounted-price"`
初期設定の手間
エージェント設定、ルール定義など初期構築には時間がかかります。ただし、一度作れば再利用可能なテンプレートになります。
まとめ
AIによるテスト生成の最大の罠は、「実装を見てテストを書く」ことです。
これを防ぐため:
- テスト生成エージェントから
Readツールを剥奪 - 仕様書(Given/When/Then)を信頼できる情報源に
- 仕様整合ルールでテスト失敗時の判断を明確化
このワークフローにより、AIが生成するテストは「実装の複製」ではなく「仕様の検証」になります。
テンプレートはGitHubで公開しています。ぜひ活用してください。 https://github.com/atomic-kanta-sasaki/-claude-test-driven-template

