test-driven-development について
コーディングエージェントに、失敗するテストを先に書き、最小限の実装を行ってからリファクタリングする手順を案内するスキルです。機能追加、バグ修正、リファクタリング、動作変更に対応し、定められた例外にはユーザーの許可が必要です。
主な機能
- 01レッド・グリーン・リファクタの実践
- 02想定した理由でテストが失敗するか確認
- 03テストを通す最小限のコードを実装
- 04個別テストと全テストの実行
- 05テスト先行のアンチパターン確認
活用シーン
- 01テスト先行で機能を追加
- 02バグを再現して再発を防止
- 03テストを維持しながらリファクタリング
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ nousresearch/ hermes-agent --skill test-driven-developmentインストール後、Claude Code で/test-driven-developmentを入力して読み込まれたか確認します。
詳細
テスト駆動開発(TDD)
概要
まずテストを書きます。失敗することを確認します。テストを通すための最小限のコードを書きます。
基本原則: テストが失敗するのを確認していないなら、そのテストが正しいものを検証しているか分かりません。
ルールの文面に違反することは、ルールの精神に違反することです。
使用するタイミング
常に:
- 新機能
- バグ修正
- リファクタリング
- 動作の変更
例外(まずユーザーに確認してください):
- 使い捨てのプロトタイプ
- 生成コード
- 設定ファイル
「今回だけTDDを省略しよう」と考えていますか? やめてください。それは自己正当化です。
鉄則
NO PRODUCTION CODE WITHOUT A FAILING TEST FIRST
テストより先にコードを書きましたか? そのコードを削除して、最初からやり直してください。
例外はありません:
- 「参考用」として残さないでください
- テストを書きながら「調整」しないでください
- 見ないでください
- 削除するとは、完全に削除することです
テストをもとに、まっさらな状態から実装してください。以上です。
レッド・グリーン・リファクタリングのサイクル
RED — 失敗するテストを書く
期待する動作を示す、最小限のテストを1つ書きます。
良いテスト:
def test_retries_failed_operations_3_times():
attempts = 0
def operation():
nonlocal attempts
attempts += 1
if attempts < 3:
raise Exception('fail')
return 'success'
result = retry_operation(operation)
assert result == 'success'
assert attempts == 3
明確な名前で、実際の動作を検証し、対象は1つです。
悪いテスト:
def test_retry_works():
mock = MagicMock()
mock.side_effect = [Exception(), Exception(), 'success']
result = retry_operation(mock)
assert result == 'success' # What about retry count? Timing?
曖昧な名前で、実際のコードではなくモックを検証しています。
要件:
- 1つのテストにつき1つの動作
- 明確で説明的な名前(名前に「and」が含まれていたら、分割してください)
- モックではなく実際のコード(本当に避けられない場合を除く)
- 名前には実装ではなく動作を記述する
RED を検証 — 失敗するのを確認する
必須です。絶対に省略しないでください。
# Use terminal tool to run the specific test
pytest tests/test_feature.py::test_specific_behavior -v
次の点を確認します:
- テストが失敗する(タイプミスによるエラーではない)
- 失敗メッセージが想定どおりである
- 機能がまだ存在しないために失敗している
テストがすぐに通りましたか? 既存の動作をテストしています。テストを修正してください。
テストでエラーが発生しましたか? エラーを修正し、正しく失敗するまで再実行してください。
GREEN — 最小限のコードを書く
テストを通すための最もシンプルなコードを書きます。それ以上は不要です。
良い例:
def add(a, b):
return a + b # Nothing extra
悪い例:
def add(a, b):
result = a + b
logging.info(f"Adding {a} + {b} = {result}") # Extra!
return result
機能を追加したり、ほかのコードをリファクタリングしたり、テストの範囲を超えて「改善」したりしないでください。
GREEN では手抜きしても構いません:
- 戻り値をハードコードする
- コピー&ペーストする
- コードを重複させる
- エッジケースを省略する
これらはリファクタリングで修正します。
GREEN を検証 — テストが通るのを確認する
必須です。
# Run the specific test
pytest tests/test_feature.py::test_specific_behavior -v
# Then run ALL tests to check for regressions
pytest tests/ -q
次の点を確認します:
- テストが通る
- ほかのテストも引き続き通る
- 出力がクリーンである(エラーや警告がない)
テストが失敗しましたか? テストではなくコードを修正してください。
ほかのテストが失敗しましたか? すぐにリグレッションを修正してください。
REFACTOR — 整理する
グリーンになってからのみ、次のことを行います:
- 重複をなくす
- 名前を改善する
- ヘルパーを抽出する
- 式を簡潔にする
常にテストが通る状態を保ってください。動作を追加しないでください。
リファクタリング中にテストが失敗した場合: すぐに元に戻してください。より小さな手順で進めてください。
繰り返す
次の動作について、次の失敗するテストを書きます。一度に1サイクルずつ進めます。
水平スライスを避ける
テストをすべて書いてから、実装をすべて書くのは避けてください。これは水平スライスです。RED が「想像したテストを山ほど書く」段階になり、GREEN が「その山を通す」段階になります。実装を通じて、どの動作やインターフェースが本当に重要かを学ぶ前にテストを設計するため、壊れやすいテストが生まれます。
代わりに、垂直のトレーサー弾を使います:
WRONG:
RED: test1, test2, test3, test4
GREEN: impl1, impl2, impl3, impl4
RIGHT:
RED→GREEN: test1→impl1
RED→GREEN: test2→impl2
RED→GREEN: test3→impl3
トレーサー弾とは、エンドツーエンドの動作を縦に切り出したものです。経路が機能することを証明し、インターフェースについて学ぶ助けとなり、次の各テストを、直前に学んだことに基づいたものにします。
順序が重要な理由
「動作を確認するために、あとでテストを書きます」
コードを書いたあとに作成したテストは、最初からすぐに合格します。すぐに合格しても、何も証明できません。
- 間違った対象をテストしている可能性があります
- 動作ではなく、実装をテストしている可能性があります
- 忘れていたエッジケースを見逃す可能性があります
- バグを検出するところを一度も確認できません
テストファーストなら、テストが失敗するのを確認でき、実際に何かをテストしていると証明できます。
「エッジケースはすべて、すでに手動でテストしました」
手動テストは場当たり的です。すべてテストしたつもりでも、次のような問題があります。
- 何をテストしたのか記録が残りません
- コードが変わったときに再実行できません
- プレッシャーのなかではケースを忘れがちです
- 「試したときは動いた」≠ 網羅的
自動テストは体系的です。毎回同じ方法で実行されます。
「X時間分の作業を削除するのは無駄です」
サンクコストの誤謬です。時間はすでに失われています。今できる選択は次のとおりです。
- 削除してTDDで書き直す(信頼性が高い)
- 残してあとからテストを追加する(信頼性が低く、バグが発生しやすい)
「無駄」なのは、信頼できないコードを残すことです。
「TDDは教条的です。現実的に考えるなら、やり方を変えるべきです」
TDDは現実的です。
- コミット前にバグを見つけます(あとからデバッグするより速い)
- リグレッションを防ぎます(テストが問題をすぐに検出します)
- 動作をドキュメント化します(テストでコードの使い方が分かります)
- リファクタリングを可能にします(自由に変更でき、テストが問題を検出します)
「現実的」な近道は、本番環境でのデバッグにつながり、結果的に遅くなります。
「あとからテストしても同じ目標を達成できます。形式ではなく、精神が大切です」
いいえ。あとから作るテストが答えるのは「これは何をするのか?」です。先に作るテストが答えるのは「これは何をすべきなのか?」です。
あとから作るテストは、実装に偏ります。必要なものではなく、作ったものをテストします。テストファーストなら、実装前にエッジケースを洗い出せます。
よくある正当化
| 言い訳 | 実情 |
|---|---|
| 「テストするには単純すぎます」 | 単純なコードでも壊れます。テストは30秒で書けます。 |
| 「あとでテストします」 | すぐに合格するテストでは、何も証明できません。 |
| 「あとからテストしても同じ目標を達成できます」 | あとから作るテスト=「これは何をするのか?」先に作るテスト=「これは何をすべきなのか?」 |
| 「すでに手動でテストしました」 | 場当たり的 ≠ 体系的。記録がなく、再実行できません。 |
| 「X時間分の作業を削除するのは無駄です」 | サンクコストの誤謬です。検証されていないコードを残すのは技術的負債です。 |
| 「参考として残し、先にテストを書きます」 | それに合わせてしまいます。それはあとからテストすることです。削除するなら完全に削除してください。 |
| 「まず調査が必要です」 | 問題ありません。調査用のコードは破棄し、TDDで始めてください。 |
| 「テストが難しい=設計が不明瞭です」 | テストの声に耳を傾けてください。テストしにくいものは、使いにくいものです。 |
| 「TDDでは遅くなります」 | TDDはデバッグより速いです。現実的に進めるなら、テストファーストです。 |
| 「手動テストのほうが速いです」 | 手動テストではエッジケースを証明できません。変更のたびに再テストすることになります。 |
| 「既存のコードにはテストがありません」 | 改善に取り組んでいるのです。変更するコードにはテストを追加してください。 |
危険信号 — STOPして、最初からやり直す
次のいずれかに気づいたら、コードを削除し、TDDでやり直してください。
- テストより先にコードを書く
- 実装のあとにテストする
- 初回実行でテストがすぐに合格する
- テストが失敗した理由を説明できない
- テストを「あとで」追加する
- 「今回だけ」と正当化する
- 「すでに手動でテストしました」
- 「あとからテストしても同じ目的を達成できます」
- 「参考として残す」または「既存のコードに合わせる」
- 「すでにX時間費やしたので、削除するのは無駄です」
- 「TDDは教条的です。私は現実的に進めています」
- 「今回は違う理由がある……」
これらはすべて、コードを削除し、TDDでやり直すべきだという意味です。
検証チェックリスト
作業を完了にする前に、次を確認してください。
- 新しく作成した関数/メソッドすべてにテストがある
- 実装前に各テストが失敗するのを確認した
- 各テストは想定どおりの理由で失敗した(機能がないためであり、誤字のためではない)
- 各テストに合格する最小限のコードを書いた
- すべてのテストに合格する
- 出力に問題がない(エラー、警告がない)
- テストでは実際のコードを使用している(避けられない場合のみモックを使用)
- エッジケースとエラーを網羅している
すべてにチェックを付けられませんか?TDDを省略しています。最初からやり直してください。
行き詰まったとき
| 問題 | 解決策 |
|---|---|
| テスト方法がわからない | 望ましい API を書きます。まずアサーションを書きます。ユーザーに尋ねます。 |
| テストが複雑すぎる | 設計が複雑すぎます。インターフェースをシンプルにします。 |
| すべてをモックする必要がある | コードの結合度が高すぎます。依存性注入を使います。 |
| テストのセットアップが大規模 | ヘルパーを抽出します。それでも複雑ですか?設計をシンプルにします。 |
Hermes Agent との統合
テストの実行
各ステップで terminal ツールを使ってテストを実行します。
# RED — verify failure
terminal("pytest tests/test_feature.py::test_name -v")
# GREEN — verify pass
terminal("pytest tests/test_feature.py::test_name -v")
# Full suite — verify no regressions
terminal("pytest tests/ -q")
delegate_task を使う場合
実装のためにサブエージェントをディスパッチするときは、目標に TDD を必須として含めます。
delegate_task(
goal="Implement [feature] using strict TDD",
context="""
Follow test-driven-development skill:
1. Write failing test FIRST
2. Run test to verify it fails
3. Write minimal code to pass
4. Run test to verify it passes
5. Refactor if needed
6. Commit
Project test command: pytest tests/ -q
Project structure: [describe relevant files]
""",
toolsets=['terminal', 'file']
)
systematic-debugging を使う場合
バグを発見しましたか?再現する失敗テストを書きます。TDD サイクルに従います。そのテストで修正を証明し、再発を防ぎます。
テストなしでバグを修正してはいけません。
テストのアンチパターン
- 実際の動作ではなくモックの動作をテストする — モックではシステムとのやり取りを検証し、テスト対象のシステムの代わりに使ってはいけません
- 実装の詳細をテストする — 内部メソッドの呼び出しではなく、動作や結果をテストします
- 成功するケースだけをテストする — エッジケース、エラー、境界条件も必ずテストします
- 壊れやすいテスト — テストでは構造ではなく動作を検証します。リファクタリングでテストが壊れてはいけません
最終ルール
Production code → test exists and failed first
Otherwise → not TDD
ユーザーの明示的な許可がない限り、例外は認められません。