test-driven-development について
test-driven-developmentは、Red-Green-Refactorサイクルでテストを先に書く開発を支援するスキルです。実装コードを書く前に、テストが想定どおりの理由で失敗することを確認するよう促します。
主な機能
- 01テストを先に書く開発サイクル
- 02実装前に想定どおりの失敗を確認
- 03テストを通す最小限のコード
- 04テスト通過後のリファクタリング
- 05デバッグと統合のガイド
活用シーン
- 01テストで動作を定義した機能開発
- 02回帰テストを使った不具合修正
- 03既存動作を確認しながらのリファクタリング
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ openai/ plugins --skill test-driven-developmentインストール後、Claude Code で/test-driven-developmentを入力して読み込まれたか確認します。
同梱ファイルagents/openai.yamlwriting-good-tests.md
詳細
テスト駆動開発(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を確認 - 失敗を見届ける
必須です。絶対に省略しないでください。
npm 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を確認 - 成功を見届ける
必須です。
npm test path/to/test.test.ts
確認事項:
- テストが成功する
- ほかのテストも引き続き成功する
- 出力がクリーンである(エラーや警告がない)
テストが失敗しましたか? テストではなく、コードを修正してください。
ほかのテストが失敗しましたか? 今すぐ修正してください。
REFACTOR - 整理する
成功を確認した後に限り:
- 重複を取り除く
- 名前を改善する
- ヘルパーを切り出す
テストを成功した状態に保ちます。動作を追加しないでください。
繰り返す
次の機能に向けて、次の失敗するテストを書きます。
良いテスト
| 品質 | 良い例 | 悪い例 |
|---|---|---|
| 最小限 | 対象は1つ。名前に「および」が含まれていますか? 分割してください。 | test('validates email and domain and whitespace') |
| 明確 | 名前が動作を説明している | test('test1') |
| 意図が分かる | 望ましいAPIを示している | コードに期待される動作が分かりにくい |
テストを新しく書く場合も変更する場合も、テストの信頼性を保つルールについて、writing-good-tests.mdを読んでください:
- テストを書く前に、そのテストを失敗させる本番コードの変更を特定する
- モックの動作ではなく、実際の動作を検証する
- テスト専用コードはテストユーティリティに置き、本番クラスには置かない
- 依存関係をモックする前に、その副作用を理解する
よくある言い訳
| 言い訳 | 実際のところ |
|---|---|
| 「テストするには単純すぎる」 | 単純なコードでも壊れます。テストは30秒で終わります。 |
| 「あとでテストする」 | あとから書いたテストはすぐに通ります。つまり、何も証明できていません。間違ったものをテストしたり、動作ではなく実装をテストしたり、忘れていた境界ケースを見落としたりする可能性があります。失敗するところを見ていないため、バグを検出できることを証明できていません。テストファーストなら、必ず失敗を確認できます。 |
| 「あとからテストしても同じ目標を達成できる(形式ではなく精神が大切)」 | あとから書くテストが答えるのは「これは何をするのか?」ですが、テストファーストが答えるのは「これは何をするべきか?」です。あとから書くテストは、すでに書いたコードに影響されるため、発見できたはずのケースではなく、覚えていたケースを検証することになります。テストが機能する証拠のないカバレッジです。 |
| 「すでに手動でテストした」 | 手動テストは場当たり的です。何をカバーしたか記録がなく、コード変更後に再実行する方法もなく、プレッシャーの中ではケースを忘れがちです。「試したときは動いた」≠網羅的です。自動テストなら毎回同じように実行できます。 |
| 「X時間分を削除するのは無駄だ」 | サンクコストの誤謬です。どちらにしても、その時間はすでに使われています。本当の選択肢は、TDDで書き直す(高い信頼性)か、そのまま残してあとからテストを追加する(低い信頼性で、バグが発生しやすい)かです。信頼できないコードを残すことこそ無駄です。 |
| 「参考として残して、テストを先に書く」 | そのコードに合わせてしまいます。それはあとからテストするのと同じです。削除するなら、完全に削除してください。 |
| 「まず調査する必要がある」 | 問題ありません。調査用のコードは破棄し、TDDで始めてください。 |
| 「テストが難しい=設計が不明瞭」 | テストの声に耳を傾けてください。テストが難しいなら、使うのも難しいということです。 |
| 「TDDは時間がかかる」 | TDDこそ現実的な方法です。コミット前にバグを検出し、リグレッションを防ぎ、安心してリファクタリングできます。「現実的」な近道は、本番環境でのデバッグにつながり、速くなるどころか遅くなります。 |
| 「手動テストのほうが速い」 | 手動テストでは境界ケースを証明できません。変更するたびに再テストが必要になります。 |
| 「既存コードにテストがない」 | いま改善しているのです。既存コードにもテストを追加してください。 |
危険信号 — 停止してやり直す
- テストより先にコードを書く
- 実装後にテストする
- テストがすぐに通る
- テストが失敗した理由を説明できない
- テストを「あとで」追加する
- 「今回だけ」と正当化する
- 「すでに手動でテストした」
- 「あとからテストしても同じ目的を達成できる」
- 「形式ではなく精神が大切」
- 「参考として残す」または「既存コードに合わせる」
- 「X時間も費やしたのだから、削除するのは無駄だ」
- 「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
リファクタリング 必要に応じて、複数のフィールドに対するバリデーションを抽出します。
検証チェックリスト
作業を完了とする前に、次を確認してください。
- 新しい関数/メソッドにはすべてテストがある
- 実装前に各テストが失敗するのを確認した
- 各テストは想定した理由で失敗した(機能不足であり、タイプミスではない)
- 各テストを通すために必要最小限のコードを書いた
- すべてのテストが通る
- 出力がクリーンである(エラーや警告がない)
- テストでは実際のコードを使用している(避けられない場合に限りモックを使用)
- 境界ケースとエラーをカバーしている
すべての項目にチェックを付けられませんか?TDDを省略しています。やり直してください。
行き詰まったとき
| 問題 | 解決策 |
|---|---|
| テスト方法がわからない | 望ましいAPIを書いてください。まずアサーションを書いてください。人間のパートナーに相談してください。 |
| テストが複雑すぎる | 設計が複雑すぎます。インターフェースをシンプルにしてください。 |
| すべてをモックする必要がある | コードの結合度が高すぎます。依存性注入を使ってください。 |
| テストのセットアップが大がかり | ヘルパーを抽出してください。それでも複雑ですか?設計をシンプルにしてください。 |
統合のデバッグ
バグを見つけましたか?そのバグを再現する失敗テストを書いてください。TDDのサイクルに従ってください。テストによって修正を証明し、リグレッションを防ぎます。
テストなしでバグを修正してはいけません。
最終ルール
Production code → test exists and failed first
Otherwise → not TDD
人間のパートナーの許可がない限り、例外は認められません。