test-driven-development について
ソフトウェア開発向けの TDD スキルです。実装前にテストが正しく失敗することを確認し、最小限のコードで通した後にリファクタリングする手順を示します。
主な機能
- 01テスト先行のレッド・グリーン・リファクタリング
- 02実装前に想定どおりの失敗を確認
- 03テストを通す最小限のコードを実装
- 04テスト通過後にリファクタリング
- 05機能追加・修正・リファクタリングの指針
活用シーン
- 01新機能の期待動作を定義
- 02失敗するテストでバグを再現して修正
- 03回帰を確認しながらコードを整理
- 04動作変更に自動テストを追加
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ makerjackie/ jackie-skills-starter --skill test-driven-developmentインストール後、Claude Code で/test-driven-developmentを入力して読み込まれたか確認します。
同梱ファイルtesting-anti-patterns.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つのことだけ。名前に「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 の検証
$ 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 必要に応じて、複数フィールドのバリデーションを抽出してください。
検証チェックリスト
作業を完了としてマークする前に:
- 新しく追加したすべての関数/メソッドにテストがある
- 実装前に、各テストが失敗するのを確認した
- 各テストが期待どおりの理由で失敗した(機能が不足している、であってタイプミスではない)
- 各テストを通すための最小限のコードを書いた
- すべてのテストが通る
- 出力がクリーン(エラーや警告がない)
- テストは実際のコードを使用している(やむを得ない場合のみモックを使用)
- エッジケースとエラーケースをカバーしている
すべてのチェックボックスを埋められませんか? それはTDDを省略したということです。最初からやり直してください。
行き詰まったとき
| 問題 | 解決策 |
|---|---|
| どうテストすればよいかわからない | 理想とするAPIを書く。まずアサーションを書く。人間のパートナーに相談する。 |
| テストが複雑すぎる | 設計が複雑すぎる。インターフェースを単純化する。 |
| すべてをモックしなければならない | コードの結合が強すぎる。依存性注入を使う。 |
| テストのセットアップが大きすぎる | ヘルパーを抽出する。それでも複雑なら、設計を単純化する。 |
デバッグとの統合
バグを見つけたら? そのバグを再現する失敗するテストを書きます。TDDのサイクルに従ってください。テストは修正を証明し、回帰を防ぎます。
テストなしでバグを修正することは決してしないでください。
テストのアンチパターン
モックやテストユーティリティを追加する際は、よくある落とし穴を避けるために @testing-anti-patterns.md を読んでください:
- 実際の動作ではなく、モックの動作をテストする
- 本番用クラスにテスト専用のメソッドを追加する
- 依存関係を理解せずにモックする
最終ルール
Production code → test exists and failed first
Otherwise → not TDD
人間のパートナーの許可なしに例外を設けることはありません。