Claude Code × TDD の正しい分業——人間が失敗テストを書き、AIが実装する「2人羽織」パターン
Claude CodeでTDD(テスト駆動開発)を回す実践ガイド。CC単独TDDの罠、人間がテストを書きCCが実装する2人羽織パターン、/testスキル活用、実例3ケース(API実装・リファクタ・バグ修正)、カバレッジ100%罠まで解説。
エンジニアのゆとです。
TDDって、理念はシンプルなんですよね。「失敗するテストを書いて、テストを通す最小実装を書いて、リファクタする」。Red-Green-Refactorのループを回すだけ。
でも実際にやろうとすると、「テストを先に書くのがしんどい」という壁にぶつかる。仕様が頭の中でまだ固まりきっていない段階で、先にテストを書くのは認知負荷が高い。結果として「TDDいいよね(やってない)」という状態に落ち着きがちだ。
Claude Codeを使い始めてから、TDDに対する向き合い方が変わった。ただし、最初にやりがちな「CCにテストも実装も全部書かせる」は罠だということも身をもって学んだ。
この記事では、Claude CodeとTDDを組み合わせるときの正しい分業パターンを、実際に使っているワークフローで解説する。
なぜAI時代こそTDDが刺さるのか
逆説的に聞こえるかもしれないが、CCのような強力なコード生成AIを使い始めるほど、TDDの必要性が上がる。
理由は単純で、生成スピードが上がった分だけ「何を作ったのか」の検証が追いつかなくなるから。
以前の開発スタイルだと、実装を自分で書いている間に自然と「あれ、このケースどうなる?」と考える余裕があった。手を動かすことが、ある種の思考の補助線になっていた。
CCが実装を書いてくれると、その補助線がなくなる。気づいたら「動いてる(ように見える)コード」が手元にあって、エッジケースへの考慮が薄い——という状態になりやすい。
TDDはこの問題への回答になる。「何を実装するか」をテストという形式で先に定義しておけば、CCが高速に実装を生成しても、検証の枠組みはすでにある。
CC単独TDDの限界——テストもAIが書くと馴れ合いになる
Claude Codeに「TDDでやって」と言うとどうなるか。
試してみると、こんな流れになる。
claude "getUserById関数をTDDで実装して"
CCは律儀にやってくれる。まずテストを書いて、次に実装を書いて、テストを通す。出力を見ると、一見きれいなRed-Green-Refactorになっている。
でもここに構造的な問題がある。
テストを書いたのもCCで、実装を書いたのもCCだ。テストと実装が同じ「頭」から出てきている。つまり、「CCが理解した仕様」の範囲内でしかテストが書かれていない。
人間が「あ、この入力値だとどうなる?」と気づくようなケースは、CCが自分で書くテストには出てこない。CCが実装しやすいように、テストが(無意識に)設計されてしまう。
もっと具体的に言うと、こんな問題が起きやすい。
- 境界値が正常値の近傍にしか置かれない(-1, 0, 1 のような典型パターン止まり)
- 外部依存(DBエラー、タイムアウト、外部API失敗)のテストが薄い
- 並行処理・レースコンディションが考慮されない
- ドメイン固有の「うちのシステムではこのケースが起きる」という知識がない
テストと実装が同じ書き手だと、盲点が共有される。これをここでは「馴れ合い」と呼んでいる。
2人羽織パターン——人間とCCの正しい分業
この問題を解決するのが、人間とCCの役割を明確に分ける「2人羽織」パターンだ。
役割分担
人間の仕事: 失敗するテストを書く
- 「この関数は何をすべきか」をテストコードで表現する
- エッジケース・異常系を自分の頭で考える
- ドメイン知識を投入する(うちのシステム固有の制約・業務ルール)
CCの仕事: テストを通す実装を書く
- 書かれたテストを全部パスする最小実装を生成する
- 人間が書いたテストの意図を読んで実装に反映させる
- リファクタフェーズでコードをきれいにする
このパターンを「2人羽織」と呼んでいる理由は、文字通り役割が分かれているから。人間がシナリオ(テスト)を決めて、CCがそれを演じる(実装する)。
なぜこれが効くのか
人間がテストを書く段階で、「この入力でこの出力になるべき」という仕様の思考を強制される。CCはその思考を代替できない。
でも「テストを通す実装を書く」という作業は、CCが得意なことだ。テストコードという明確なゴールが与えられているので、CCの出力品質が高くなる。
テスト(仕様定義)は人間が担当して、実装(仕様充足)はCCが担当する。それぞれが得意なことをやっている。
/test スキルとSub-agentsでTDDループを高速化する
Claude CodeにはTDDループを加速するための機能がいくつかある。
/test スキルの使い方
/test コマンドはプロジェクトのテストランナーを自動検出して実行する。
# 特定ファイルのテストだけ走らせる
/test src/services/user-service.test.ts
# 全テスト + カバレッジレポート
/test --coverage
自分のワークフローだと、テストを書いた後にこのコマンドを実行して「Redであること」を確認してからCCに実装を依頼する。
「Redの確認」を怠ると、最初からPassしてしまうテスト(意味のないテスト)を書いた時に気づけない。
Sub-agentsでParallelにテストを回す
複数のエンドポイントやモジュールに対してTDDをやる場合、Sub-agentsを使うと効率が上がる。
claude "以下の3つのサービスに対して並行でTDD実装を進めて。
それぞれ僕が書いたテストを通すだけ。テストを書き換えないこと。
- src/services/auth-service.test.ts → src/services/auth-service.ts
- src/services/notification-service.test.ts → src/services/notification-service.ts
- src/services/billing-service.test.ts → src/services/billing-service.ts"
「テストを書き換えないこと」という制約を明示するのが重要だ。CCはテストが難しいと判断すると、テストの方を緩和しようとすることがある。
実例1: APIエンドポイント実装(新規開発)
ECサイトの在庫チェックAPIを新規実装するケースで、2人羽織パターンを使った流れ。
ステップ1: 人間がテストを書く
// src/api/inventory.test.ts
import { checkInventory } from './inventory';
describe('checkInventory', () => {
it('在庫がある場合、在庫数と利用可能フラグを返す', async () => {
const result = await checkInventory('SKU-001');
expect(result).toEqual({
sku: 'SKU-001',
available: true,
quantity: expect.any(Number),
});
expect(result.quantity).toBeGreaterThan(0);
});
it('在庫がゼロの場合、available: false を返す', async () => {
const result = await checkInventory('SKU-OUT-OF-STOCK');
expect(result.available).toBe(false);
expect(result.quantity).toBe(0);
});
it('存在しないSKUはNotFoundErrorをスローする', async () => {
await expect(checkInventory('SKU-NONEXISTENT')).rejects.toThrow('NotFoundError');
});
it('在庫数が1の場合(ぎりぎり)でもavailable: trueを返す', async () => {
const result = await checkInventory('SKU-LAST-ONE');
expect(result.available).toBe(true);
expect(result.quantity).toBe(1);
});
it('SKUが空文字の場合はValidationErrorをスローする', async () => {
await expect(checkInventory('')).rejects.toThrow('ValidationError');
});
// 業務ルール: 予約済み在庫は利用可能数から除外される
it('予約済み在庫がある場合、実在庫から予約数を引いた値を返す', async () => {
// DB: quantity=10, reserved=7 → available_quantity=3
const result = await checkInventory('SKU-WITH-RESERVATION');
expect(result.quantity).toBe(3);
expect(result.available).toBe(true);
});
});
このテストで注目してほしいのは、SKU-WITH-RESERVATION のケースだ。「予約済み在庫は実在庫から引く」というのは、うちのシステム固有の業務ルール。CCに「在庫チェック関数を書いて」と頼んでも、この要件は出てこない。
ステップ2: テストがRedであることを確認
/test src/api/inventory.test.ts
Cannot find module './inventory' が出ればOK。実装ファイルがまだないので当然だ。
ステップ3: CCに実装を依頼
claude "src/api/inventory.test.ts のテストを全部通す実装を
src/api/inventory.ts に書いて。
テストコードは変更しないこと。
DBクライアントは src/db/client.ts にある既存のものを使って"
CCがやること:
- テストケースを全部読んで、必要なインターフェースを推定
checkInventory関数のシグネチャを決める- 正常系・異常系・業務ルール(予約在庫)を全部カバーする実装を書く
- DBクライアントの既存実装を読んで、クエリを組み立てる
ここで「テストコードは変更しないこと」という制約が効いてくる。
ステップ4: Green確認 → Refactor
/test src/api/inventory.test.ts
全テストがPassしたら、リファクタをCCに頼む。
claude "inventory.ts の実装をリファクタして。
機能は変えずに、可読性と保守性を上げること。
テストは全部Passのままにすること"
実例2: リファクタ(レガシーコードの改善)
既存の手続き的なコードをリファクタする時にTDDを使うパターン。
レガシーコードは往々にして「動いてるけど触りたくない」状態になっている。テストがない、副作用がある、依存が絡み合っている。
このケースでは、「リファクタ前の動作をテストで固める」というアプローチを使う。
ステップ1: 既存動作をテストでロックする
// src/legacy/order-processor.test.ts
import { processOrder } from './order-processor';
describe('processOrder(リファクタ前の動作をロック)', () => {
it('正常注文を処理して注文IDを返す', async () => {
const order = {
userId: 'user-123',
items: [{ sku: 'SKU-001', quantity: 2 }],
totalAmount: 4000,
};
const orderId = await processOrder(order);
expect(orderId).toMatch(/^ORD-/);
});
it('在庫不足の場合はInsufficientStockErrorをスローする', async () => {
const order = {
userId: 'user-123',
items: [{ sku: 'SKU-OUT-OF-STOCK', quantity: 1 }],
totalAmount: 2000,
};
await expect(processOrder(order)).rejects.toThrow('InsufficientStockError');
});
it('合計金額がゼロの場合はValidationErrorをスローする', async () => {
await expect(
processOrder({ userId: 'user-123', items: [], totalAmount: 0 })
).rejects.toThrow('ValidationError');
});
});
このテストは、既存コードの「現在の動作」をロックするためのもの。リファクタ中にこのテストが壊れたら、動作が変わったということだ。
ステップ2: CCにリファクタを依頼
claude "src/legacy/order-processor.ts をリファクタして。
src/legacy/order-processor.test.ts のテストが全部Passしていることを
リファクタ後も保証すること。
手続き的なコードを、責務ごとに分離したクラス設計に変えてほしい"
CCはテストをリファクタの制約として使いながら、コードを整理していく。
実例3: バグ修正(再発防止まで)
バグを直す時に、TDDを使うと再発防止まで一気にできる。
バグレポートが来た時のワークフロー:
ステップ1: バグを再現するテストを書く
// src/utils/date-formatter.test.ts に追加
it('2月29日(うるう年)を正しくフォーマットできる', () => {
const date = new Date('2024-02-29');
expect(formatDate(date)).toBe('2024/02/29');
});
it('12月31日を正しくフォーマットできる(年末バグの再発防止)', () => {
const date = new Date('2024-12-31');
expect(formatDate(date)).toBe('2024/12/31');
});
「このバグが起きる入力でテストを書く」ことで、修正後に同じバグが再発しても自動で検出できるようになる。
ステップ2: テストがRedであることを確認
/test src/utils/date-formatter.test.ts
このステップでテストがGreenになってしまったら、バグの再現条件が間違っている。
ステップ3: CCに修正を依頼
claude "src/utils/date-formatter.test.ts のうち、
以下の2テストがFailしている。
- '2月29日(うるう年)を正しくフォーマットできる'
- '12月31日を正しくフォーマットできる(年末バグの再発防止)'
date-formatter.ts を修正してこの2テストをPassさせて。
既存の全テストもPassのままにすること"
バグの再現テストがあるので、CCは「何が修正できたか」を自分で確認しながら実装できる。
テストカバレッジの罠——CCに任せると100%を目指して破綻する
これはかなりやってしまった失敗なので共有する。
「カバレッジを上げて」とCCに頼むと、CCは忠実にカバレッジを上げようとする。具体的にはこんなことが起きる。
- テストのためだけのgetter/setterを実装コードに追加し始める
- 内部実装の詳細(プライベートメソッドの呼ばれ方)を直接テストしようとする
- 到達不可能なコードパスへのアクセスするためのテストダブルを乱造する
結果として、カバレッジが95%になるが、テストスイートがリファクタの度に壊れるようになる。実装の詳細に密結合したテストが大量にできるから。
正しい指示の仕方:
claude "テストカバレッジを上げて。
ただし以下の制約を守って:
- テストのためだけにプロダクションコードを変更しない
- プライベートメソッドを直接テストしない(公開インターフェースを通してテスト)
- カバレッジ目標は80%。100%は目指さない"
カバレッジは「テストが何をカバーしているか」の指標であって、「テストの品質」の指標ではない。この区別をCCへの指示でも明確にしておく必要がある。
ハマりどころと対策
実際に2人羽織パターンを運用してみてぶつかった問題をまとめる。
CCがテストを「合理的に」書き換えようとする
最もよく起きる問題。CCは「このテストケースは仕様として矛盾している」「このエラーメッセージの期待値は正確じゃない」などの理由でテストを修正しようとすることがある。
対策: 毎回のプロンプトに「テストコードを変更しないこと」を明記する。CCがテストを変えようとしてきたら、「なぜ変えようとしているか」を聞いて、仕様の議論はテストを書き直す(人間が)という流れにする。
モックの設計が噛み合わない
人間がテストを書いた時点でのモックの想定と、CCが書いた実装のインターフェースが合わなくなることがある。
対策: テストを書く前に、インターフェースの設計だけをCCと一緒に決める。
claude "このユースケースを実装するとしたら、
どんな関数シグネチャ・インターフェースが適切か提案して。
実装は書かなくていい。インターフェースだけ"
インターフェースが決まってからテストを書く。実装はその後。
テストの実行環境がCCのコンテキストに入らない
プロジェクトの規模が大きくなると、「テストを走らせると何かの外部サービスに接続しようとして失敗する」という問題が出てくる。
CCがこれを解決しようとすると、テスト設定を複雑にしすぎることがある。
対策: プロジェクトルートに CLAUDE.md を置いて、テスト実行の前提条件・モックする外部依存を明記しておく。
# テスト実行の前提
- DBはインメモリ(SQLite)を使用: TEST_DB_URL=:memory:
- 外部API(Stripe, SendGrid)はすべてモック必須
- テスト実行: npm test(Vitest)
- カバレッジ: npm run test:coverage
CCはこれを読んでからテスト実装を書くので、設定の迷いがなくなる。
全テストのPASS確認をCCに任せると嘘をつく
「テストが全部通ることを確認して」と頼んだ時に、CCが実際にテストを実行せずに「確認しました」と言うことがある(実行に時間がかかるケースで特に起きやすい)。
対策: /test コマンドでテスト実行を明示的に指示する。CCの「確認しました」を信用しない。
/test --run --reporter=verbose
テスト結果のアウトプットが出てから次の作業に進む習慣にしている。
まとめ
Claude Code × TDDの要点をまとめる。
- CCにテストも実装も任せる「全自動TDD」は馴れ合いになる。テストと実装が同じ頭から出ると、盲点が共有される
- 人間がテストを書いて(仕様定義)、CCが実装を書く(仕様充足)という2人羽織パターンが正しい分業
/testコマンドでRedの確認をちゃんとやる。テストが最初からGreenだったら意味がない- カバレッジ100%を目指すな。CCは指示通りに動くので、目標設定が間違っていると間違った方向に全力を出す
- 「テストコードは変更しないこと」は毎回明記する
TDDは「テストを先に書く」だけの話じゃなくて、「何を作るかを先に定義する」思考の話でもある。その定義を人間がやって、充足をCCがやる——という分業が、AI時代のTDDの正しい形だと思っている。
関連記事:


