test-driven-development について
ソフトウェア開発向けの TDD スキルで、最小限の実装とリファクタリングの前に、テストを書いて失敗を確認することを求めます。段階的なサイクルやテスト実行の規律、検証チェックリストでテストファーストの作業を整えます。
主な機能
- 01Red-Green-Refactor サイクル
- 02振る舞いテストを一つずつ進行
- 03実装前に失敗を確認
- 04テスト実行とプロセス終了の規則
- 05テスト設計とモックのガイド
- 06カバレッジと完了チェックリスト
活用シーン
- 01新機能の実装前にテストを作成
- 02不具合修正後に回帰動作を確認
- 03テストを維持しながらコードをリファクタリング
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ romiluz13/ cc10x --skill test-driven-developmentインストール後、Claude Code で/test-driven-developmentを入力して読み込まれたか確認します。
同梱ファイル
SKILL.md と一緒にインストールされる参考資料やスクリプト。
references/integration-and-live-proof.mdtest-data-and-mocks.mdtesting-patterns.md詳細
テスト駆動開発(TDD)
概要
まずテストを書きます。失敗することを確認します。テストを通すための最小限のコードを書きます。
基本原則: テストが失敗するのを確認していないなら、そのテストが正しいものを検証しているかどうかは分かりません。
ルールの文言に違反することは、ルールの精神に違反することです。
参照ファイル
現在のテストサイクルに必要な参照ファイルだけを読みます。
references/testing-patterns.md:命名、AAA 構造、僅差の否定ケース、動作への着目、アンチパターンの確認references/test-data-and-mocks.md:ファクトリー、モックの境界、一般的な境界モック、環境変数と時刻の扱いreferences/integration-and-live-proof.md:単体テストだけでは不十分な場合や、実際の API、シードデータ、ブラウザ操作、負荷検証が計画に含まれる場合
使用するタイミング
常に使用:
- 新機能
- バグ修正
- リファクタリング
- 動作の変更
例外(人間のパートナーに確認してください):
- 使い捨てのプロトタイプ
- 生成コード
- 設定ファイル
「今回だけ TDD を省略してもいいのでは?」と思ったら、立ち止まってください。それは正当化です。
鉄則
NO PRODUCTION CODE WITHOUT A FAILING TEST FIRST
テストより先にコードを書きましたか? 削除して、最初からやり直してください。
例外はありません:
- 「参考用」として残さないでください
- テストを書きながら「調整」しないでください
- 見ないでください
- 削除とは、完全に削除することです
テストをもとに、新たに実装してください。それだけです。
テストプロセスの規律(重要)
問題: テストランナー(Vitest、Jest)はデフォルトでウォッチモードになり、プロセスがいつまでも終了しないことがあります。
必須ルール:
- 常に実行モードを使ってください — ウォッチモードは決して起動しないでください。
- Vitest:
npx vitest run(npx vitestではありません) - Jest:
CI=true npx jestまたはnpx jest --watchAll=false - npm スクリプト:
CI=true npm testまたはnpm test -- --run
- Vitest:
- すべてのテストコマンドには、CI=true のプレフィックスを付けることを推奨します:
CI=true npm test - TDD サイクルの完了後、孤立したプロセスがないことを確認してください:
pgrep -f "vitest|jest" || echo "Clean" - 見つかったら終了してください:
pkill -f "vitest" 2>/dev/null || true
レッド・グリーン・リファクタリング
┌─────────┐ ┌─────────┐ ┌───────────┐
│ RED │──────>│ GREEN │──────>│ REFACTOR │
│ (Fail) │ │ (Pass) │ │ (Clean) │
└─────────┘ └─────────┘ └───────────┘
^ │
│ │
└────────────────────────────────────┘
Next Feature
垂直スライス(重要)
WRONG (horizontal — all tests then all code):
RED: test1, test2, test3, test4, test5
GREEN: impl1, impl2, impl3, impl4, impl5
RIGHT (vertical — one feature at a time):
RED->GREEN: test1->impl1
RED->GREEN: test2->impl2
RED->GREEN: test3->impl3
すべてのテストを先に書き、その後ですべてを実装するのは避けてください。 この方法では、質の悪いテストが生まれます。
- まとめて書いたテストは、実際の動作ではなく、_想像上の_動作を検証します
- ユーザーに見える動作ではなく、物事の_形_をテストすることになります
- テストが実際の変更に反応しなくなり、動作が壊れても通り、問題がなくても失敗するようになります
- 実装を理解する前にテスト構造を決めてしまい、見通しを誤ります
正しい方法: 1 つのテスト → 1 つの実装 → 繰り返す。各テストは、前のサイクルで得た知見を踏まえて作成します。
RED - 失敗するテストを書く
期待する動作を示す、最小限のテストを 1 つ書きます。
良い例:
test('retries failed operations 3 times', async () => {
let attempts = 0;
const operation = () => {
attempts++;
if (attempts < 3) throw new Error('fail');
return 'success';
};
const result = await retryOperation(operation);
expect(result).toBe('success');
expect(attempts).toBe(3);
});
明確な名前で、実際の動作を 1 つ検証しています
悪い例:
test('retry works', async () => {
const mock = jest.fn()
.mockRejectedValueOnce(new Error())
.mockRejectedValueOnce(new Error())
.mockResolvedValueOnce('success');
await retryOperation(mock);
expect(mock).toHaveBeenCalledTimes(3);
});
曖昧な名前で、コードではなくモックを検証しています
要件:
- 動作は 1 つ
- 明確な名前
- 実際のコードを使用する(避けられない場合を除き、モックは使わない)
RED を確認 - 失敗する様子を見る
必須です。決して省略しないでください。
CI=true npm test path/to/test.test.ts
次を確認してください:
- テストが失敗する(エラーではない)
- 失敗メッセージが想定どおりである
- 機能が未実装であるために失敗している(タイプミスが原因ではない)
テストが通りましたか? 既存の動作をテストしています。テストを修正してください。
テストでエラーが発生しましたか? エラーを修正し、正しく失敗するまで再実行してください。
GREEN - 最小限のコードを書く
テストを通すための最もシンプルなコードを書きます。
良い例:
async function retryOperation<T>(fn: () => Promise<T>): Promise<T> {
for (let i = 0; i < 3; i++) {
try {
return await fn();
} catch (e) {
if (i === 2) throw e;
}
}
throw new Error('unreachable');
}
テストを通すために必要な分だけです
悪い例:
async function retryOperation<T>(
fn: () => Promise<T>,
options?: {
maxRetries?: number;
backoff?: 'linear' | 'exponential';
onRetry?: (attempt: number) => void;
}
): Promise<T> {
// YAGNI - You Ain't Gonna Need It
}
過剰な設計です
機能を追加したり、別のコードをリファクタリングしたり、テストの範囲を超えて「改善」したりしないでください。テストの値をハードコードしないでください。すべての入力で機能する汎用的なロジックを実装してください。
GREEN を確認 - テストが通る様子を見る
必須です。
CI=true npm test path/to/test.test.ts
次を確認してください:
- テストが通る
- ほかのテストも引き続き通る
- 出力に問題がない(エラーや警告がない)
テストが失敗しましたか? テストではなくコードを修正してください。
ほかのテストが失敗しましたか? すぐに修正してください。
REFACTOR - 整理する
グリーンになった後だけ、次の作業を行います。
- 重複を取り除く
- 名前を改善する
- ヘルパーを抽出する
テストが通る状態を保ってください。動作は追加しないでください。
繰り返す
次の機能について、次の失敗するテストを書きます。
良いテスト
| 品質 | 良い例 | 悪い例 |
|---|---|---|
| 最小限 | 1つのことだけをテストします。名前に「and」が含まれている場合は分割します。 | test('validates email and domain and whitespace') |
| 明確 | 名前が動作を説明しています | test('test1') |
| 意図を示す | 望ましい API の使い方を示します | コードがどう動くべきかを分かりにくくします |
テスト構造の詳細、境界値に近い負のテスト、動作と内部実装の違い、コードの臭いのチェックについては、references/testing-patterns.mdをお読みください。
ファクトリー、モック、環境変数や時間の扱いについては、
references/test-data-and-mocks.mdをお読みください。
順序が重要な理由
「動作確認のため、あとでテストを書きます」
コードを書いたあとに作成したテストは、すぐに通ります。すぐに通ることは何も証明しません。
- 間違ったものをテストしている可能性があります
- 動作ではなく、実装をテストしている可能性があります
- 忘れていたエッジケースを見落とす可能性があります
- テストがバグを検出する様子を見たことがありません
テストファーストならテストが失敗するのを確認でき、実際に何かをテストしていると証明できます。
「エッジケースはすべて手動でテストしました」
手動テストは場当たり的です。すべてテストしたと思っていても、次の問題があります。
- 何をテストしたか記録がありません
- コード変更後に再実行できません
- プレッシャーの中でケースを忘れやすくなります
- 「試したときは動いた」≠ 網羅的
自動テストは体系的です。毎回同じ方法で実行されます。
「X 時間分の作業を削除するのは無駄です」
サンクコストの誤謬です。費やした時間はすでに戻りません。今の選択肢は次のとおりです。
- 削除して TDD で書き直す(さらに X 時間、高い信頼性)
- そのままにしてあとからテストを追加する(30 分、信頼性が低く、バグが発生しやすい)
本当の「無駄」は、信頼できないコードを残すことです。実際のテストがない動作するコードは技術的負債です。
危険信号 - STOPして最初からやり直す
次のようなことに気づいたら、
- テストより先にコードを書く
- 実装後にテストする
- テストがすぐに通る
- テストが失敗した理由を説明できない
- テストの追加を「あとで」にする
- 「今回だけ」と正当化する
- 「すでに手動でテストした」
- 「あとからテストしても同じ目的を達成できる」
- 「大切なのは形式ではなく精神だ」
- 「参考として残す」または「既存のコードを適応させる」
- 「すでに X 時間費やしたから、削除するのは無駄だ」
- 「TDD は教条的すぎる。私は現実的に対応している」
- 「これは違う。なぜなら……」
これらはすべて、コードを削除してTDDで最初からやり直すべきという意味です。
正当化を防ぐ
| 言い訳 | 実態 |
|---|---|
| 「テストするには単純すぎる」 | 単純なコードも壊れます。テストは30秒で書けます。 |
| 「あとでテストする」 | すぐに通るテストは何も証明しません。 |
| 「あとからテストしても同じ目的を達成できる」 | テストをあとから書く=「これは何をするのか?」。テストファースト=「これは何をすべきか?」。 |
| 「すでに手動でテストした」 | 場当たり的 ≠ 体系的。記録がなく、再実行もできません。 |
| 「X 時間分の作業を削除するのは無駄だ」 | サンクコストの誤謬です。検証されていないコードを残すことは技術的負債です。 |
| 「参考として残し、テストを先に書く」 | それをもとに手を加えるでしょう。それはあとからテストすることです。削除とは削除することです。 |
| 「まず調査する必要がある」 | かまいません。調査用のコードは捨て、TDDで始めてください。 |
| 「テストが難しい=設計が不明確」 | テストから学びましょう。テストしにくいものは使いにくいものです。 |
| 「TDDをすると遅くなる」 | TDDはデバッグより速いです。現実的な対応とはテストファーストです。 |
| 「手動テストのほうが速い」 | 手動テストではエッジケースを証明できません。変更のたびに再テストすることになります。 |
| 「既存のコードにテストがない」 | そのコードを改善するのです。既存のコードにもテストを追加してください。 |
例:バグ修正
バグ: 空のメールアドレスが受け入れられる
RED
test('rejects empty email', async () => {
const result = await submitForm({ email: '' });
expect(result.error).toBe('Email required');
});
REDを検証
$ npm test
FAIL: expected 'Email required', got undefined
GREEN
function submitForm(data: FormData) {
if (!data.email?.trim()) {
return { error: 'Email required' };
}
// ...
}
GREENを検証
$ npm test
PASS
REFACTOR 必要であれば、複数のフィールドの検証を切り出します。
検証チェックリスト
作業を完了とする前に確認してください。
- 新しい関数/メソッドすべてにテストがある
- 実装前に各テストが失敗するのを確認した
- 各テストは想定どおりの理由で失敗した(タイプミスではなく、機能がないため)
- 各テストを通すための最小限のコードを書いた
- すべてのテストが通る
- 出力に問題がない(エラーや警告がない)
- テストでは実際のコードを使用している(避けられない場合のみモックを使用)
- エッジケースとエラーを網羅している
- ハングしたテストプロセスがない(pgrep -f "vitest|jest" の結果が空)
すべての項目にチェックできませんか? TDDをスキップしています。最初からやり直してください。
統合と実稼働環境での検証
承認された計画やリスクプロファイルがローカル環境での動作を超える場合は、
references/integration-and-live-proof.mdを読んでください。
次の項目に依存するタスクでは、ユニットテストだけでは不十分です。
- 実際のAPI呼び出し
- データのシードまたはリセット
- ブラウザーまたはワーカーのオーケストレーション
- サービスをまたぐ副作用
- 負荷またはストレス時の動作
このような場合は、内部の反復サイクルではTDDを維持し、外部の検証では検証の深度を高めてください。
カバレッジのしきい値(プロジェクトのデフォルト)
目標:コードカバレッジ80%以上。対象は以下のとおりです。
- 分岐:80%
- 関数:80%
- 行:80%
- ステートメント:80%
確認方法: npm run test:coverageまたは同等のコマンドを使用します。
しきい値未満の場合は? 完了を宣言する前に、不足しているテストを追加してください。
テストの優先順位付け
カバレッジ80%を達成するには、どのテストを優先するかを意識して選ぶ必要があります。次の項目に注力してください。
- ユーザー向けの重要な経路(認証、決済、データの整合性)
- 分岐が複数ある複雑なロジック
- これまでに不具合が発生したことのあるコード(回帰が起きやすい箇所)
コードが「単純に見える」からといって、テストを省略しないでください。単純なコードでも壊れます。80%という目標は最低ラインであり、上限ではありません。
行き詰まったとき
| 問題 | 解決策 |
|---|---|
| テスト方法がわからない | 望ましいAPIを書きます。先にアサーションを書きます。人間のパートナーに相談します。 |
| テストが複雑すぎる | 設計が複雑すぎます。インターフェースをシンプルにします。 |
| すべてをモックする必要がある | コードの結合度が高すぎます。依存性注入を使います。 |
| テストのセットアップが大きすぎる | ヘルパーを切り出します。それでも複雑なら、設計をシンプルにします。 |
テスト容易性を考慮した設計(テストが難しい場合)
テストを書くのが難しい場合は、インターフェースを見直す必要があります。
-
依存関係を受け取る。自分で作成しない
- テストしやすい例:
function processOrder(order, paymentGateway) {} - テストしにくい例:
function processOrder(order) { const gw = new StripeGateway(); }
- テストしやすい例:
-
副作用を発生させず、結果を返す
- テストしやすい例:
function calculateDiscount(cart): Discount {} - テストしにくい例:
function applyDiscount(cart): void { cart.total -= discount; }
- テストしやすい例:
-
表面積を小さくする — メソッドが少ないほど必要なテストが減り、パラメーターが少ないほどセットアップがシンプルになります。
振る舞いに焦点を当てる
オブジェクトの中身ではなく、オブジェクトがどのように連携するかをテストしてください。テストが.state、.length、またはプライベートフィールドを調べているなら、それは構造をテストしています。内部の動作が変わっても振る舞いが変わらない場合に、そのテストは失敗します。
| テスト対象 | 正しい例 | 誤った例 |
|---|---|---|
| 関数の出力 | expect(calculate(input)).toBe(result) | expect(calculator.internalCache).toContain(...) |
| コンポーネントの振る舞い | expect(screen.getByText('Saved')).toBeTruthy() | expect(component.state.saved).toBe(true) |
| サービスとの連携 | expect(response.status).toBe(201) | expect(service.callCount).toBe(1) |
これはすでに、Test Smells表の「実装のテスト」という問題点で示されています。これを基本的な視点にしてください。すべてのアサーションは「ユーザー/呼び出し元が何を観察したか」に答えるべきであり、「内部で何が起きたか」に答えるべきではありません。
エージェント間のテスト契約
CC10xが複数のエージェント間で作業を割り振る場合(プランナーがテスト仕様を書き、ビルダーが実装し、レビュアーが検証する場合)、テストファイルが契約そのものです。
- プランナーは、計画内のテスト名とアサーションで期待される振る舞いを定義します
- ビルダーは先にテストを書き、すべて成功するように実装します。テストファイルが契約の達成を証明します
- レビュアーは同じテストを再実行します。合格なら契約が履行され、不合格なら契約が破られています
契約の内容を文章で重複して書かないでください。テストファイルが要件を表しているなら、テストファイルが要件です。
出力形式
## TDD Cycle
### Requirements
[What functionality is being built]
### RED Phase
- Test: [test name]
- Command: `npm test -- --grep "test name"`
- Result: exit 1 (FAIL as expected)
- Failure reason: [function not defined / expected X got Y]
### GREEN Phase
- Implementation: [summary]
- File: [path:line]
- Command: `npm test -- --grep "test name"`
- Result: exit 0 (PASS)
### REFACTOR Phase
- Changes: [what was improved]
- Command: `npm test`
- Result: exit 0 (all tests pass)
最終ルール
Production code → test exists and failed first
Otherwise → not TDD
人間のパートナーの許可がない限り、例外は認められません。