test-driven-development について
ソフトウェアの動作を実装または修正する開発者向けのテスト駆動開発スキルです。動作を変える本番コードを書く前にテストの失敗を確認し、レッド・グリーン・リファクタリングのサイクルに従います。
主な機能
- 01動作変更前に失敗するテストを必須化
- 02レッド・グリーン・リファクタリングの手順
- 03有効なテスト失敗の基準を提示
- 04レガシーコードの特性テストを解説
- 05決定的なテストのルールを提示
活用シーン
- 01テストを先に書いて機能を実装
- 02修正前に不具合を再現
- 03特性テストを使ったレガシーコードのリファクタリング
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ izyanrajwani/ agent-skills-library --skill test-driven-developmentインストール後、Claude Code で/test-driven-developmentを入力して読み込まれたか確認します。
同梱ファイルreferences/tdd-philosophy.mdreferences/testing-anti-patterns.md
詳細
テスト駆動開発
まずテストを書きます。失敗するのを確認します。合格するための最小限のコードを書きます。リファクタリングします。
基本原則: テストが失敗するのを確認していないなら、そのテストが正しいものを検証しているかどうかは分かりません。
鉄の掟
NO BEHAVIOR-CHANGING PRODUCTION CODE WITHOUT A FAILING TEST FIRST
テストより先にコードを書きましたか? そのコードをすべて削除してください。テストをもとに、新しく実装します。
リファクタリングは例外です: リファクタリングのステップでは、動作ではなく構造を変更します。テストは常に成功したままにします。新たに失敗するテストは必要ありません。
レッド・グリーン・リファクタリングのサイクル
RED ──► Verify Fail ──► GREEN ──► Verify Pass ──► REFACTOR ──► Verify Pass ──► Next RED
│ │ │
▼ ▼ ▼
Wrong failure? Still failing? Broke tests?
Fix test, retry Fix code, retry Fix, retry
RED - 失敗するテストを書く
1つの動作に対して、最小限のテストを1つ書きます。
良い例:
test('retries failed operations 3 times', async () => {
let attempts = 0;
const operation = async () => {
attempts++;
if (attempts < 3) throw new Error('fail');
return 'success';
};
const result = await retryOperation(operation);
expect(result).toBe('success');
expect(attempts).toBe(3);
});
明確な名前で、実際の動作をテストし、観察可能な結果を検証しています
悪い例:
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
テストは正しい理由で失敗しなければなりません。許容される RED の状態:
- アサーションの失敗(期待する動作が実装されていない)
- コンパイル/型エラー(関数がまだ存在しない)
許容されないもの:実行時のセットアップエラー、インポートの失敗、環境の問題。
テストがすぐに成功しましたか? 既存の動作をテストしています。テストを修正してください。 誤った理由でエラーになりますか? エラーを修正し、正しく失敗するまで再実行してください。
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'; }
): Promise<T> { /* YAGNI */ }
テストの要件を超えて過剰に設計されています
テストが求めるものだけを書きます。追加機能や「改善」は加えません。
GREEN を確認 - 合格するのを確認する
必須です。
npm test path/to/test.test.ts
確認事項:テストが成功する。他のすべてのテストも成功する。出力に問題がない(エラーや警告がない)。
テストが失敗しましたか? テストではなくコードを修正してください。 他のテストが失敗しましたか? 続行する前に修正してください。
REFACTOR - 整理する
GREEN の後にのみ実施します:重複を取り除きます。名前を改善します。ヘルパーを抽出します。
テストは常に成功したままにします。新しい動作は追加しません。
繰り返す
次の動作に対する、次の失敗するテストに進みます。
良いテスト
最小限: テストごとに1つのことを検証します。名前に「and」が含まれていますか? 分割してください。❌ test('validates email and domain and whitespace')
明確: 名前で動作を説明します。❌ test('test1')
意図を示す: 実装の詳細ではなく、望ましい API の使い方を示します。
例:バグ修正
バグ: 空のメールアドレスが許可される
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: パターンが繰り返される場合は、検証ヘルパーを抽出します。
危険信号 - 停止してやり直す
以下に1つでも当てはまる場合は、コードを削除して TDD でやり直してください:
- テストより先にコードを書いた
- テストがすぐに成功する(既存の動作をテストしている)
- テストが失敗した理由を説明できない
- 「今回だけ」や「これは別だ」と正当化している
- テストを書きながら、コードを「参考用」として残している
- 「テストは後から書いても同じ目的を達成できる」と主張している
行き詰まったとき
| 問題 | 解決策 |
|---|---|
| テスト方法が分からない | 望ましい API を書きます。まずアサーションを書きます。 |
| テストが複雑すぎる | 設計が複雑すぎます。インターフェースをシンプルにします。 |
| すべてをモックする必要がある | コードの結合度が高すぎます。依存性注入を導入します。 |
| テストのセットアップが大きすぎる | ヘルパーを抽出します。それでも複雑なら、設計をシンプルにします。 |
レガシーコード(既存のテストがない場合)
鉄の掟(「削除してやり直す」)が適用されるのは、テストなしで自分が書いた新しいコードです。テストのない既存コードには、特性テストを使用します:
- 現在の動作を記録するテストを書く(たとえ「誤った」動作でも)
- テストを実行して、実際の出力を確認する
- アサーションを実際の動作に合わせて更新する(これが「ゴールデンマスター」です)
- これで、リファクタリングの安全網ができます
- 新しい動作を変更するときは TDD を適用する
特性テストは既存の動作を固定し、安全にリファクタリングできるようにします。これは導入の足掛かりであり、恒久的な状態ではありません。
不安定さに関するルール
テストは決定論的でなければなりません。ユニットテストでは、以下を禁止します。
- 実際のスリープ/遅延 → フェイクタイマー(
vi.useFakeTimers()、jest.useFakeTimers())を使用します - 実時間の時計 → 時計を注入し、注入した時刻に対してアサーションします
- Math.random() → シードを設定するか、RNG を注入します
- ネットワーク呼び出し → 境界でモックするか、MSW を使用します
- ファイルシステムの競合状態 → 一意な名前の一時ディレクトリを使用します
不安定なテストが見つかった場合は、修正するか削除してください。不安定なテストは、テストスイート全体への信頼を損ないます。
デバッグ時の統合
バグを見つけた場合は、まずそれを再現する失敗テストを書いてください。その後、TDD サイクルに従います。このテストによって修正を検証し、回帰を防ぎます。
計画:テストリスト
サイクルに取りかかる前に、次に書く予定のテストを 3~10 個、2 分かけてリストアップします。こうすることで、初期のテストによって後戻りできなくなるような、局所最適な設計を防げます。
リトライ関数のテストリストの例:
- 失敗時に N 回リトライする
- 成功時に結果を返す
- 最大リトライ回数に達した後、例外をスローする
- 試行の間に onRetry コールバックを呼び出す
- バックオフの遅延を守る
リストの順に取り組みます。学びに応じてテストを追加、削除してください。
テストのアンチパターン
モック、依存関係、テストユーティリティを含むテストを書く際は、よくある落とし穴(モックの動作をテストすることや、本番クラスにテスト専用のメソッドを追加することなど)について、references/testing-anti-patterns.md を参照してください。
哲学と合理化
よくある反論(「テストは後で書く」「作業を削除するのは無駄だ」「TDD は独断的だ」など)への詳細な反論については、references/tdd-philosophy.md を参照してください。
最終ルール
Production code exists → test existed first and failed first
Otherwise → not TDD