validation-first-approach について
開発者やコーディングエージェント向けの TDD スキルです。実装前にテストが意図どおり失敗することを確認し、Red-Green-Refactor の手順を促します。
主な機能
- 01本番コードより先に失敗テスト
- 02RED と GREEN の確認
- 03最小限の実装後にリファクタリング
- 04バグ修正とエッジケースのガイダンス
活用シーン
- 01テストを先に書いて新機能を開発
- 02バグを再現してから修正
- 03テストでリファクタリングの回帰を確認
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ qodex-ai/ ai-agent-skills --skill validation-first-approachインストール後、Claude Code で/validation-first-approachを入力して読み込まれたか確認します。
同梱ファイルtesting-anti-patterns.md
詳細
テス駆動開発(TDD)
概要
まずテストを書きます。それが失敗するのを確認します。次に、テストを通す最小限のコードを書きます。
中核となる原則: テストが失敗するのを見ていなければ、そのテストが正しいことを検証しているかどうかは分かりません。
ルールの文言に違反することは、ルールの趣旨に違反することです。
使用するタイミング
常に:
- 新機能
- バグ修正
- リファクタリング
- 動作の変更
例外(人間のパートナーに確認してください):
- 使い捨てのプロトタイプ
- 生成されたコード
- 設定ファイル
「今回だけTDDをスキップしよう」と考えていますか? 立ち止まってください。それは合理化です。
鉄則
NO PRODUCTION CODE WITHOUT A FAILING TEST FIRST
テストより先にコードを書いてしまったら? 削除してください。最初からやり直します。
例外はありません:
- 「参考」として残さない
- テストを書きながら「修正して流用」しない
- それを見ない
- 削除とは削除のことです
テストから新たに実装します。以上です。
Red-Green-Refactor
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
人間のパートナーの許可なしに例外を設けることはありません。