test-driven-development について
テスト駆動開発を実践する開発者向けのスキルです。失敗するテストを先に書き、最小限の実装を行ってからリファクタリングします。テストが意図どおり失敗し、成功することを確認して進めます。
主な機能
- 01Red-Green-Refactor サイクル
- 02意図した理由で失敗するか確認
- 03合格に必要な最小限の実装
- 04テスト合格後のリファクタリング
- 05チェックリストとデバッグ指針
活用シーン
- 01新機能をテストファーストで開発
- 02不具合を再現して回帰を防止
- 03テストを維持しながらリファクタリング
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ camacho/ ai-skills --skill test-driven-developmentインストール後、Claude Code で/test-driven-developmentを入力して読み込まれたか確認します。
詳細
テスト駆動開発(TDD)
概要
まずテストを書きます。失敗することを確認します。テストを通すための最小限のコードを書きます。
基本原則: テストが失敗するところを確認していなければ、そのテストが正しい対象をテストしているかどうかはわかりません。
ルールの字面に反することは、ルールの精神に反することです。
使用する場面
常に使用します:
- 新機能
- バグ修正
- リファクタリング
- 動作の変更
例外(人間のパートナーに相談してください):
- 使い捨てのプロトタイプ
- 生成コード
- 設定ファイル
「今回だけTDDを省略しよう」と考えていますか? やめてください。それは自己正当化です。
鉄の掟
NO PRODUCTION CODE WITHOUT A FAILING TEST FIRST
テストより先にコードを書きましたか? そのコードを削除してください。最初からやり直します。
例外はありません:
- 「参考用」として残さないでください
- テストを書きながら「調整」しないでください
- 見ないでください
- 削除とは、本当に削除することです
テストをもとに、最初から実装してください。それだけです。
レッド・グリーン・リファクタリング
digraph tdd_cycle {
rankdir=LR;
red [label="RED\nWrite failing test", shape=box, style=filled, fillcolor="#ffcccc"];
verify_red [label="Verify fails\ncorrectly", shape=diamond];
green [label="GREEN\nMinimal code", shape=box, style=filled, fillcolor="#ccffcc"];
verify_green [label="Verify passes\nAll green", shape=diamond];
refactor [label="REFACTOR\nClean up", shape=box, style=filled, fillcolor="#ccccff"];
next [label="Next", shape=ellipse];
red -> verify_red;
verify_red -> green [label="yes"];
verify_red -> red [label="wrong\nfailure"];
green -> verify_green;
verify_green -> refactor [label="yes"];
verify_green -> green [label="no"];
refactor -> verify_green [label="stay\ngreen"];
verify_green -> next;
next -> red;
}
RED - 失敗するテストを書く
期待する動作を示す、最小限のテストを1つ書きます。
```typescript 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つ
</Good>
<Bad>
```typescript
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を検証 - 失敗するところを確認する
必須です。絶対に省略しないでください。
プロジェクトのテストコマンドを実行します(例:pnpm test — 正しいコマンドは package.json または AGENTS.md で確認してください):
pnpm test path/to/test.test.ts
次を確認します:
- テストが失敗する(エラーではない)
- 失敗メッセージが想定どおり
- 機能がないために失敗する(タイプミスではない)
テストが成功しましたか? 既存の動作をテストしています。テストを修正してください。
テストでエラーが発生しましたか? エラーを修正し、正しく失敗するまで再実行してください。
GREEN - 最小限のコードを書く
テストを通すための最もシンプルなコードを書きます。
```typescript async function retryOperation(fn: () => Promise): Promise { for (let i = 0; i < 3; i++) { try { return await fn(); } catch (e) { if (i === 2) throw e; } } throw new Error('unreachable'); } ``` 合格に必要な分だけ ```typescript async function retryOperation( fn: () => Promise, options?: { maxRetries?: number; backoff?: 'linear' | 'exponential'; onRetry?: (attempt: number) => void; } ): Promise { // YAGNI } ``` 過剰な設計機能を追加したり、他のコードをリファクタリングしたり、テストの範囲を超えて「改善」したりしないでください。
GREENを検証 - 成功するところを確認する
必須です。
プロジェクトのテストコマンドを実行します(例:pnpm test — 正しいコマンドは package.json または AGENTS.md で確認してください):
pnpm test path/to/test.test.ts
次を確認します:
- テストが成功する
- 他のテストも引き続き成功する
- 出力に問題がない(エラーや警告がない)
テストが失敗しましたか? テストではなく、コードを修正してください。
他のテストが失敗しましたか? すぐに修正してください。
REFACTOR - 整理する
グリーンになった後だけ、次のことを行います:
- 重複を取り除く
- 名前を改善する
- ヘルパーを抽出する
テストが引き続き成功する状態を保ってください。動作を追加しないでください。
繰り返す
次の機能に対して、次に失敗するテストを書きます。
良いテスト
| 品質 | 良い例 | 悪い例 |
|---|---|---|
| 最小限 | 対象は1つ。名前に「and」がありますか? 分割してください。 | test('validates email and domain and whitespace') |
| 明確 | 名前で動作を説明している | test('test1') |
| 意図がわかる | 望ましいAPIを示している | コードが何をすべきかをわかりにくくしている |
なぜ順序が重要なのか
「動作することは後でテストして確認すればいい」
コードの後に書かれたテストは、即座に通ります。即座に通ることは、何も証明しません:
- 間違ったものをテストしているかもしれない
- 振る舞いではなく、実装をテストしているかもしれない
- 忘れていたエッジケースを見逃すかもしれない
- そのテストがバグを検出するところを一度も見ていない
テストファーストでは、テストが失敗するところを必ず確認することになり、そのテストが実際に何かをテストしていることが証明されます。
「エッジケースはすべて手動でテスト済みだ」
手動テストは場当たり的です。すべてをテストしたと思っていても:
- どのようなテストを行ったかの記録がない
- コードが変更されたときに再実行できない
- プレッシャーの下では、ケースを忘れやすい
- 「試したときは動いた」は、網羅的であることを意味しない
自動テストは体系的です。毎回同じ方法で実行されます。
「X時間の作業を削除するのは無駄だ」
サンクコストの誤謬です。その時間はすでに失われています。今あなたが選べるのは:
- 削除して、TDD で書き直す(さらにX時間、信頼度は高い)
- そのまま残して、後からテストを追加する(30分、信頼度は低く、バグがある可能性が高い)
「無駄」なのは、信頼できないコードを残すことのほうです。実際のテストがない動作するコードは、技術的負債です。
「TDD は教条的であり、実用的であるとは状況に適応することだ」
TDD こそが実用的です:
- コミット前にバグを発見できる(事後のデバッグより速い)
- リグレッションを防げる(テストが即座に破損を検出する)
- 振る舞いを文書化できる(テストがコードの使い方を示す)
- リファクタリングを可能にする(自由に変更でき、テストが破損を検出する)
「実用的」な近道は、本番環境でのデバッグを意味し、結果としてより遅くなります。
「後からのテストでも同じ目的は達成できる。大切なのは形式ではなく精神だ」
いいえ。後からのテストは「これは何をするのか?」に答え、テストファーストは「これは何をすべきか?」に答えます。
後からのテストは、あなたの実装に偏っています。要求されていることではなく、作ったものをテストします。発見されたエッジケースではなく、思い出せたエッジケースを検証します。
テストファーストは、実装前にエッジケースの発見を強制します。後からのテストは、あなたがすべてを思い出せたかを検証しますが、実際には思い出せていません。
後から書いた30分のテストは、TDD ではありません。カバレッジは得られても、テストが機能することの証明は失われます。
よくある言い訳
| 言い訳 | 実際 |
|---|---|
| 「単純すぎてテストする必要がない」 | 単純なコードも壊れます。テストには30秒しかかかりません。 |
| 「後でテストする」 | 即座に通るテストは、何も証明しません。 |
| 「後からのテストでも同じ目標を達成できる」 | 後からのテスト=「これは何をするのか?」。テストファースト=「これは何をすべきか?」。 |
| 「すでに手動でテスト済み」 | 場当たり的 ≠ 体系的。記録がなく、再実行もできません。 |
| 「X時間を削除するのは無駄」 | サンクコストの誤謬です。検証されていないコードを残すことは、技術的負債です。 |
| 「参考として残し、先にテストを書く」 | 結局それを適応させることになります。それは後からのテストです。削除とは削除することです。 |
| 「まず探索する必要がある」 | 問題ありません。探索したコードは捨て、TDD で始めてください。 |
| 「テストが難しい=設計が不明確」 | テストの声に耳を傾けてください。テストしにくい=使いにくい。 |
| 「TDD は開発を遅くする」 | TDD はデバッグより速いです。実用的とはテストファーストのことです。 |
| 「手動テストのほうが速い」 | 手動では、エッジケースを証明できません。変更のたびに再テストすることになります。 |
| 「既存のコードにはテストがない」 | 既存のコードを改善しているのです。既存のコードにテストを追加してください。 |
危険信号 - 立ち止まり、最初からやり直してください
- テストより先にコードを書く
- 実装の後にテストを書く
- テストが即座に通る
- テストが失敗した理由を説明できない
- テストを「後で」追加する
- 「今回だけ」と正当化する
- 「すでに手動でテストした」
- 「後からのテストでも同じ目的を達成できる」
- 「大切なのは形式ではなく精神だ」
- 「参考として残す」または「既存のコードを適応させる」
- 「すでにX時間費やした。削除するのは無駄だ」
- 「TDD は教条的だ。私は実用的に行動している」
- 「これは違う。なぜなら…」
これらはすべて、こう意味します: コードを削除し、TDD で最初からやり直してください。
例: バグ修正
バグ: 空のメールアドレスが受け入れられる
RED
test('rejects empty email', async () => {
const result = await submitForm({ email: '' });
expect(result.error).toBe('Email required');
});
RED の検証
$ pnpm test
FAIL: expected 'Email required', got undefined
GREEN
function submitForm(data: FormData) {
if (!data.email?.trim()) {
return { error: 'Email required' };
}
// ...
}
GREEN の検証
$ pnpm test
PASS
REFACTOR 必要に応じて、複数フィールドのバリデーションを抽出してください。
検証チェックリスト
作業を完了としてマークする前に、以下を確認してください。
- 新しい関数/メソッドにはすべてテストがある
- 実装を始める前に、各テストが失敗することを確認した
- 各テストは想定どおりの理由で失敗した(機能が未実装であり、タイプミスではない)
- 各テストを通すために必要最小限のコードを書いた
- すべてのテストが成功する
- 出力に問題がない(エラーや警告がない)
- テストでは実際のコードを使用している(避けられない場合のみモックを使用する)
- エッジケースとエラーを網羅している
すべての項目にチェックを付けられませんか?TDD を省略しています。最初からやり直してください。
行き詰まったとき
| 問題 | 解決策 |
|---|---|
| テスト方法がわからない | 望ましい API を書きます。まずアサーションを書きます。人間のパートナーに相談します。 |
| テストが複雑すぎる | 設計が複雑すぎます。インターフェースをシンプルにします。 |
| すべてをモックする必要がある | コードの結合度が高すぎます。依存性注入を使用します。 |
| テストのセットアップが大規模すぎる | ヘルパーを抽出します。それでも複雑なら、設計をシンプルにします。 |
統合のデバッグ
バグを見つけましたか?再現する失敗テストを書きます。TDD のサイクルに従います。テストによって修正を検証し、再発を防ぎます。
テストなしでバグを修正してはいけません。
テストのアンチパターン
モックやテストユーティリティを追加する際は、よくあるアンチパターンに注意してください。
- 実際の動作ではなく、モックの動作をテストする
- テスト専用のメソッドを本番クラスに追加する
- 依存関係を理解せずにモックする
最終ルール
Production code → test exists and failed first
Otherwise → not TDD
人間のパートナーの許可がない限り、例外は認められません。