Claude Codeでテスト駆動開発(TDD)を回す手順 — 失敗するテストからStop hookでの締め方まで
Claude CodeでのTDDは、失敗するテストを先に用意し、その合否をClaude自身が読める状態にすることから始まります。プロンプトの型、/goalとStop hookでの締め方、つまずきやすい点をまとめます。
Claude CodeのTDDは合否の判定をClaudeに渡すこと
Claude Codeにおけるテスト駆動開発とは、実装より先に失敗するテストを用意し、その合否をClaude自身が読み取れる状態にしたうえで実装を任せる進め方です。テストを先に書く点は人間が回すTDDと変わりません。ただし狙いが違います。人間のTDDが設計のリズムを作るためのものだとすれば、こちらはClaudeが「終わった」と判断する基準を外側から与えるためのものです。
理由ははっきりしています。Claudeは作業が完了したように見えた時点で止まります。自分で走らせて結果を読める検証手段がないと、「完了したように見えるか」が唯一の判断材料になります。すると間違いは人間が気づくまで待つことになり、検証ループの担当が人間に戻ってきます。合否を返す仕組みを渡すと、このループは閉じます。実装し、検証を走らせ、結果を読み、通るまで直す。ここまでが手を離れます。
検証手段はテストである必要すらありません。ビルドの終了コード、リンター、出力をフィクスチャと突き合わせるスクリプト、デザインと比較するブラウザーのスクリーンショット。会話のなかでClaudeが読み取れるシグナルを返すものなら、どれも同じ役割を果たします。そのなかでテストが扱いやすいのは、粒度を細かく刻めることと、失敗メッセージがそのまま次の指示になることです。
始める前に用意するもの
前提は4つです。どれも数分で片付きますが、抜けると後の手順が空振りします。
-
テストが1コマンドで走ること。
npm testでもpytest -qでも構いませんが、引数なしで走って終了コードを返す形にしておきます。Claudeが繰り返し叩くのはこのコマンドです。手順が複数に分かれていると、そのたびに判断が挟まって精度が落ちます。 -
テストコマンドをCLAUDE.mdに書くこと。CLAUDE.mdは会話の開始時に毎回読み込まれるファイルで、Claudeがコードから推測できない情報を置く場所です。
/initを実行すると、ビルドシステムやテストフレームワークを検出した雛形が生成されます。読み込まれたかどうかは/contextで確認できます。
書く内容は絞ります。長いCLAUDE.mdは肝心の指示が埋もれて無視されるので、各行について「これを消したらClaudeが間違えるか」を基準に判断します。テスト周りなら、この程度で足ります。
# テスト
- テストは `npm test` で走らせる
- 全体ではなく単一ファイルのテストを優先する(速度のため)
- 新しいテストはモックを使わず実物で書く-
テストコマンドの実行を許可しておくこと。既定では、システムを変更しうる操作のたびに承認を求められます。テストは何十回も走るので、
/permissionsでテストコマンドを許可リストに入れるか、承認そのものを分類器に任せる自動モードを使います。10回目の承認を過ぎたあたりから、人間は内容を読まずにクリックしているだけになります。 -
締め方を使うならバージョンを確認すること。後述する
/goalはv2.1.139以降で使えます。claude --versionで確認できます。
手順1:失敗するテストだけを書かせる
最初のターンでは、テストだけを書かせて実装には手を付けさせません。ここを曖昧にすると、テストと実装が同じターンで生まれ、テストが実装に引きずられます。
src/auth/session.ts のセッション期限切れ時の挙動にテストを書いてください。
期限切れトークンでrefreshを呼ぶと AuthExpiredError が投げられ、
既存セッションが破棄されることを確認したい。
モックは使わず、テストだけを書いて実装には触れないでください。
書き終えたらテストを走らせて、失敗することを見せてください。指示に3つ入れています。検証条件を具体例で渡すこと、モックを避けること、実装に触れないこと。3つ目が特に効きます。「テストを書いて」とだけ言うと、Claudeは親切に実装まで進めてしまうことがあります。
「走らせて失敗を見せてください」も外せません。失敗を確認しない限り、そのテストが本当に対象の挙動を見ているかは分かりません。通ってしまった場合の扱いは後半で触れます。
テストが期待どおり落ちたら、この時点で一度コミットします。実装が入る前のテストだけの状態が残っていると、後から「テストが実装に合わせて緩められていないか」を差分で確認できます。
不確かなのが「どういうテストが必要か」の段階なら、先にプランモードを挟む手もあります。Shift+Tab を押すと default → acceptEdits → plan の順に切り替わります。プランモード中は、ファイルを読み、調べるためのコマンドは走らせますが、ソースの編集はしません。使いどころはPlan modeの使い分けにまとめています。
手順2:実装は文脈を分けて走らせる
テストが揃ったら実装です。ここで効くのが、テストを書いたセッションとは別の文脈で実装させることです。
新しい文脈は、自分が直前に書いたコードへの肩入れを持ちません。同じセッションで続けると、Claudeは自分が書いたテストの意図を覚えているぶん、テストが甘い箇所をそのまま素通りさせがちです。片方にテストを書かせ、もう片方にそれを通すコードを書かせる分担は、レビューを別セッションに渡すのと同じ考え方です。
分け方は3通りあり、隔離の強さが違います。
| 分け方 | 隔離される範囲 | 向く場面 |
|---|---|---|
| サブエージェント | 隔離される範囲コンテキストのみ(同じ作業ツリー) | 向く場面1ファイルに収まる実装 |
別セッション(/clear後) | 隔離される範囲コンテキストのみ | 向く場面複数ファイルにまたがる実装 |
| git worktree | 隔離される範囲コンテキストと作業ツリー | 向く場面並行して別の作業も動かすとき |
サブエージェントに任せる場合は、そう明示します。
サブエージェントを使って、test/auth/session.test.ts が通るように
src/auth/session.ts を実装してください。テストファイルは変更しないこと。「テストファイルは変更しないこと」を毎回添えます。これがないと、実装が難しいときにテスト側を緩めて通してしまう経路が残ります。サブエージェントは自分のコンテキストでファイルを読み、結果だけを返すので、メインの会話が実装の探索で埋まらない利点もあります。詳しい定義方法はサブエージェントの作り方を参照してください。
手順3:テストの通過をターンの終了条件にする
ここまでは指示で回す方法です。指示は毎回書く必要があり、書き忘れれば効きません。締め方を仕組みに移すと、離席していても止まる場所が変わります。
セッションを走らせ続ける手段は3つあり、次のターンが始まるきっかけが違います。
| 手段 | 次のターンが始まるとき | 止まるとき |
|---|---|---|
/goal | 次のターンが始まるとき前のターンが終わったとき | 止まるときモデルが条件の充足を確認したとき |
/loop | 次のターンが始まるとき一定の時間が経過したとき | 止まるとき自分で止めるか、Claudeが完了と判断したとき |
| Stop hook | 次のターンが始まるとき前のターンが終わったとき | 止まるとき自分が書いたスクリプトまたはプロンプトが判断したとき |
TDDで使うのは1行目と3行目です。/loop は時間間隔で再実行する仕組みなので、テストの合否とは連動しません。
/goal で条件を満たすまで走らせる
/goal は完了条件を設定するコマンドです。ターンが終わるたびに小さく速いモデル(既定はHaiku)が条件を満たしたかを判定し、満たしていなければ制御を返さず次のターンを始めます。条件が満たされると自動的に解除されます。
/goal test/auth のテストが全て通り、npm run lint が終了コード0で終わること条件を書くときの制約が1つあります。判定するモデルはコマンドを実行せず、ファイルも独自には読みません。会話にすでに現れた内容だけを見て判断します。したがって条件は、Claude自身の出力で証明できる形にします。「test/authのテストが全て通る」が成立するのは、Claudeがテストを走らせた結果が記録として残るからです。条件は4,000文字まで書けます。
走らせすぎを防ぐなら、条件のなかに上限を入れておきます。「または20ターンで停止」のような句を足すと、進捗をそれに対して報告するようになります。状況は引数なしの /goal で確認でき、/goal clear で途中解除できます(stop off reset none cancel も同じ働きです)。
CIやバッチで非対話に回す
/goal は非対話モードでも使えます。-p で渡すと、1回の起動でループを最後まで回します。
claude -p "/goal test/auth のテストが全て通ること" \
--output-format stream-json --verbose既定のテキスト出力だと条件を満たすまで何も表示されないので、ターン数が多いゴールは止まっているように見えます。--output-format stream-json --verbose を付けると、ループの進行がメッセージ単位で流れます。途中で打ち切るときはCtrl+Cです。
Stop hookで決定論的に締める
/goal はセッション限りの設定です。プロジェクト全体に効かせるならStop hookを使います。Stop hookはClaudeが応答を終えるタイミングで発火し、ターンの終了を止めて会話を続けさせられます。
設定は .claude/settings.json に書きます。
{
"hooks": {
"Stop": [
{
"hooks": [
{
"type": "command",
"command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/require-tests.sh",
"timeout": 600
}
]
}
]
}
}スクリプト側は、テストが落ちたら終了コード2で抜けます。終了コード2はブロック扱いになり、標準エラー出力の内容がそのまま理由としてClaudeに渡ります。終了コード1は非ブロック扱いなので、止めたいときに1で抜けると素通りします。
#!/usr/bin/env bash
input=$(cat)
# すでにStop hookで継続中かどうかは stop_hook_active で分かる
continuing=$(printf '%s' "$input" | jq -r '.stop_hook_active // false')
if ! npm test --silent; then
if [ "$continuing" = "true" ]; then
echo "テストがまだ通っていません。直せないなら理由を述べて終了してください。" >&2
else
echo "テストが失敗しています。原因を直してから終了してください。" >&2
fi
exit 2
fihookの入力には stop_hook_active が渡ります。Stop hookによる継続の結果として今のターンが始まったかどうかを示す値です。止め続けても解決しない条件かどうかを、ここで見分けます。テスト通過を条件にする限り、落ちている間はブロックし続けるのが狙いどおりなので、上の例では文言を変えるだけにしています。何回か試して直らないなら諦めさせたい場合は、ここで終了コード0で抜けます。
例では jq を使っていますが、入っていなくてもスクリプトは壊れません。値が空になって分岐が効かなくなるだけで、テストが落ちていればこれまでどおりブロックします。
終了コードの代わりに、終了コード0で {"decision": "block", "reason": "..."} を標準出力に返す書き方もできます。CLAUDE.mdの記述が助言にとどまるのに対し、hookは条件を満たさない限り必ず走る点が違います。書き方の全体像はhooksの設定ガイドに、設定ファイルのキー一覧はsettings.jsonリファレンスにあります。
編集ごとにテストを走らせる(PostToolUse)
Stop hookが動くのはターンの終わりです。1ターンのあいだに10ファイル編集すれば、テストが走るのは10回の編集が終わったあと1回だけになります。編集のたびに結果を返したいなら、ツールの実行後に発火するPostToolUseを使います。
matcherはツール名に対して当たるので、ファイルを書き換える2つを指定します。
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/run-tests.sh"
}
]
}
]
}
}挙動がStop hookと決定的に違う点が1つあります。PostToolUseはツールが成功したあとに発火するので、終了コード2で抜けても編集は取り消されません。標準エラー出力の内容が、すでに完了した操作へのフィードバックとしてClaudeに渡るだけです。テストが落ちたことを早く知らせる用途には向きますが、危険な編集を止める用途には使えません。止めたいならツール実行前のPreToolUseです。
並列で複数ファイルを編集する場合は、粒度をもう一段変えられます。PostToolBatchは並列呼び出しのまとまりが解決したあとに1回だけ発火し、こちらは終了コード2で次のモデル呼び出しの前にループを止められます。編集1件ずつテストを回すと重いプロジェクトでは、こちらのほうが現実的です。
| 発火点 | 走る回数 | 終了コード2の効果 |
|---|---|---|
| PostToolUse | 走る回数編集ごと | 終了コード2の効果編集は取り消されず、内容がフィードバックされる |
| PostToolBatch | 走る回数並列バッチごとに1回 | 終了コード2の効果次のモデル呼び出し前にループを止める |
| Stop | 走る回数ターンの終わり | 終了コード2の効果ターンの終了を止めて会話を続けさせる |
締め方の使い分け早見表
4つの締め方は、設定の手間と効く範囲が違います。同じ場面で全部を使う必要はありません。
| 締め方 | 効く範囲 | 準備 | 判定するもの |
|---|---|---|---|
| プロンプトに書く | 効く範囲そのターンのみ | 準備なし | 判定するものClaude自身 |
/goal | 効く範囲そのセッション | 準備コマンド1行 | 判定するもの別の小さいモデル |
| Stop hook | 効く範囲設定ファイルの適用範囲 | 準備スクリプト作成 | 判定するもの自分で書いた判定 |
| 検証サブエージェント | 効く範囲呼んだときだけ | 準備プロンプトまたは定義ファイル | 判定するもの別文脈のモデル |
離席して戻ったら終わっている、という状態を作るのは下2つです。プロンプトに書く方法は準備が要らないぶん、書き忘れると何も効きません。
よくあるつまずき
書かせたテストが最初から通ってしまう
失敗を確認する手順を飛ばすと起きます。原因は2つに割れます。対象の挙動がすでに実装済みか、テストが対象を見ていないかです。前者なら実装は不要で、後者はテストの作り直しになります。どちらかを確かめずに実装へ進むと、通っているテストの下で壊れたコードが増えます。
モックで通してしまう
「テストを書いて」とだけ頼むと、外部依存をモックで置き換えた形になりがちです。モックは実装の内部構造を写し取るので、リファクタリングのたびに壊れ、通っていても本番の挙動を保証しません。プロンプトに「モックを避ける」と明示するのが最短の対処です。避けられない依存があるときは、どこまでを実物で通すかを先に決めておきます。
/goal の条件が判定できない形になっている
「実装が正しいこと」「バグがないこと」のような条件は判定できません。判定するモデルはツールを呼ばず、会話に現れた内容だけを見るからです。テストの実行結果、ビルドの終了コード、ファイル数、空になったキューのように、Claudeの出力として残るものを条件にします。
hookを入れたのに何も起きない
.claude/settings.json に書いたつもりが、別の階層に置かれている場合に起きます。プロジェクト設定として読まれるのはプロジェクトルート、つまりリポジトリのルートの .claude/ です。設定済みのhookは /hooks で一覧できます。
置き場所が正しいのに動かないなら、hookそのものが無効化されている可能性があります。設定のどこかの階層で disableAllHooks が有効になっていると、すべてのhookが止まります。管理者設定に allowManagedHooksOnly がある場合も、利用者・プロジェクト・プラグインのhookが遮断されます。
/goal はこれに加えてもう1つ条件があります。評価役がhookの仕組みの一部なので、信頼ダイアログを承認したワークスペースでしか動きません。/goal については、上記3つのどれに当たっても理由をコマンドが返すので、黙って何も起きないわけではありません。
巻き戻しでテストが戻らない
/rewind やEsc2回で戻せるのは、そのセッションのチェックポイントに記録された変更だけです。Bashコマンド経由の変更や外部プロセスの変更は記録されません。テストの生成をシェルスクリプト任せにしていると、巻き戻しても消えないファイルが残ります。
手順2でサブエージェントに実装させた場合も戻りません。サブエージェントの編集はファイル編集ツールを通っていても、セッションのチェックポイントの外側に着地します。この記事の進め方はサブエージェントを勧めているので、ここは実質的にgit頼みになります。テストだけをコミットしてから実装に入る手順1の締めは、この意味でも効いてきます。なおEsc2回が巻き戻しになるのは入力欄が空のときで、書きかけの文字があるとそちらが消えるだけです。
CLAUDE.mdが長すぎてテスト規約が無視される
同じ指示を何度出しても守られないときは、ファイルが長くなりすぎて指示が埋もれている可能性があります。CLAUDE.mdは毎回読み込まれるので、常に効かせたい短い規約だけを置き、たまにしか使わない手順はスキルへ移します。どの階層に何を置くかはメモリの3階層で整理しています。
よくある質問
テストを先に書かせると開発は遅くなりますか
1周あたりのターン数は増えます。一方で、実装を書いてから人間が確認して差し戻す往復が減ります。人間が検証ループに入ったままだと、間違いは気づくまで滞留するので、離席して回せる時間が長いほど差が出ます。
既存のテストがないプロジェクトでも始められますか
これから触る範囲だけにテストを置く形なら始められます。全体のカバレッジを先に上げる必要はありません。触る関数の周辺だけ失敗するテストを用意し、そこから広げていく進め方が現実的です。
/goal とStop hookはどちらを使えばよいですか
その場限りの作業なら /goal、チームで常に効かせたいならStop hookです。/goal はセッション内だけで有効なコマンドで、Stop hookは設定ファイルに置かれ適用範囲のすべてのセッションで動きます。判定を決定論的にしたい場合もhook側になります。
テストが通ったあとにコードの質まで見てもらえますか
差分だけを新しい文脈で読ませる方法があります。/code-review は現在の差分をサブエージェントで読み、バグを報告して返します。ただし、指摘を探すよう頼まれたモデルは、健全なコードにも何か見つけて返しがちです。正しさと要件に関わる指摘だけを対象にすると伝えておくと、過剰な抽象化を防げます。
自動テストのないUI変更でも同じ形にできますか
スクリーンショットを合否のシグナルとして使えます。実装後にスクリーンショットを撮って元のデザインと比較し、差分を列挙して直す、という指示の形です。合否を返す何かがあれば構造は変わりません。
まとめ
Claude CodeでのTDDは、テストを先に書くこと自体より、Claudeが自分で合否を読める状態を先に作ることに意味があります。手順としては、失敗するテストだけを書かせて失敗を確認し、テストだけをコミットしてから、別の文脈で実装を走らせる。ここまでで往復はかなり減ります。
離席したまま完了させたいなら、締め方を仕組みに移します。セッション限りなら /goal、プロジェクト全体ならStop hook。どちらもターンの終わりで判定が走り、条件を満たすまで制御が戻りません。8回連続でブロックされる失敗は人間が読む、という切り分けも最初から決まっています。
うまくいかないときは、たいてい入口が原因です。テストが失敗することを確認したか。モックで通していないか。条件はClaudeの出力で証明できる形か。この3つを先に見ると、原因の切り分けが速くなります。全体像はClaude Code完全ガイドにまとめています。
関連する記事
Claude Code をもっと見る →Claude Codeとは — Anthropicのエージェント型AIコーディングCLI完全ガイド
CursorからClaude Codeへの移行ガイド — 設定の引き継ぎから「Tab補完がない」問題までの実務手順
Claude Codeベストプラクティス — Anthropicが示す自走エージェントの設計原則と運用パターン
Claude Code Hooksの設定方法 — 初めて書く人の完全手順
Claude Codeプラグイン(Plugins)完全ガイド — /pluginの使い方・マーケットプレイス・自作と配布
Claude Code設定ガイド — settings.jsonの主要フィールド・環境変数・実戦レシピ