tdd について
tddは、言語やテストフレームワークを問わず使えるテスト駆動開発のスキルで、公開インターフェースを通じた振る舞いの検証を重視します。内部実装に依存するテストを避け、テストと実装を段階的に進める方法を示します。
主な機能
- 01公開インターフェースで振る舞いを検証
- 02システム境界のみをモック
- 03垂直スライスで段階的に作業
- 04単一のエンドツーエンドテストから開始
- 05リファクタリング前にGREENへ到達
活用シーン
- 01ユーザー向けの振る舞いをテスト
- 02リファクタリング時の脆いテストを削減
- 03機能を段階的に実装して検証
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ inkeep/ team-skills --skill tddインストール後、Claude Code で/tddを入力して読み込まれたか確認します。
詳細
テスト駆動開発の方法論
優れたテストを書くための普遍的な原則です。フレームワーク、言語、リポジトリを問わず適用できます。プロジェクトの設定、コントリビューター向けガイド、既存のテストファイル、CI/CD 設定、リポジトリレベルの AI スキルやルールを確認し、リポジトリ固有のテストツール、コマンド、規約を把握して従ってください。
核となる原則
テストが検証するのは、実装の詳細ではなく、公開インターフェースを介した振る舞いです。コードは全面的に変更できますが、振る舞いが変わらない限り、テストが壊れるべきではありません。
優れたテストは、公開 API を介して実際のコードパスを実行します。システムが「どのように」動作するかではなく、「何を」するかを記述します。優れたテストは仕様のように読めます。「ユーザーが有効なカートで購入手続きできる」とあれば、どのような機能があるのかが正確にわかります。内部構造を気にしないため、こうしたテストはリファクタリング後も維持できます。
悪いテストは実装と密結合しています。内部の協調関係をモックしたり、プライベートメソッドをテストしたり、データベースに直接問い合わせるなど外部的な手段で検証したりします。警告のサインは、振る舞いが変わっていないのにリファクタリングでテストが壊れることです。
良いテストと悪いテスト
// GOOD: Tests observable behavior through the interface
test("created user is retrievable", async () => {
const user = await createUser({ name: "Alice" });
const retrieved = await getUser(user.id);
expect(retrieved.name).toBe("Alice");
});
// BAD: Bypasses interface to verify via implementation detail
test("createUser saves to database", async () => {
await createUser({ name: "Alice" });
const row = await db.query("SELECT * FROM users WHERE name = ?", ["Alice"]);
expect(row).toBeDefined();
});
// GOOD: Tests the outcome
test("user can checkout with valid cart", async () => {
const cart = createCart();
cart.add(product);
const result = await checkout(cart, paymentMethod);
expect(result.status).toBe("confirmed");
});
// BAD: Tests the mechanism
test("checkout calls paymentService.process", async () => {
const mockPayment = vi.spyOn(paymentService, "process");
await checkout(cart, payment);
expect(mockPayment).toHaveBeenCalledWith(cart.total);
});
実装と密結合したテストの危険信号:
- 内部の協調関係をモックする(外部との境界は除く)
- プライベートメソッドをテストする
- 呼び出し回数や呼び出し順序を検証する
- 振る舞いが変わっていないのに、リファクタリングでテストが壊れる
- テスト名が「何を」ではなく「どのように」を記述している(「ユーザーが Y できる」ではなく「X を呼び出す」)
モックの考え方
システムの境界でのみモックします:
- 外部 API(決済、メール、サードパーティーサービス)
- データベース(場合によっては。利用できる場合はテスト用データベースを優先してください)
- 時刻/乱数
- ファイルシステム(場合によっては)
独自のモジュール、内部の協調関係、自分で制御できるものはモックしないでください。何かをテストするために内部モジュールをモックする必要があるなら、モックを増やす必要があるのではなく、インターフェース設計に改善の余地があるというサインです。
境界でモックしやすい設計にする:
// Easy to mock — dependency is injected
function processPayment(order, paymentClient) {
return paymentClient.charge(order.total);
}
// Hard to mock — dependency is internal
function processPayment(order) {
const client = new StripeClient(process.env.STRIPE_KEY);
return client.charge(order.total);
}
垂直スライス(アンチパターン:水平スライス)
テストをすべて先に書いてから、実装をすべて行うのは避けてください。これは「水平スライス」と呼ばれ、弱いテストを生みます。
- テストをまとめて書くと、_実際の_振る舞いではなく、_想像上の_振る舞いをテストします
- ユーザー向けの振る舞いではなく、物事の_形_をテストすることになります
- テストが実際の変更に反応しなくなり、振る舞いが壊れても成功し、問題がなくても失敗するようになります
- 実装を理解する前に、テストの構造を決めてしまいます
正しいアプローチ — トレーサーバレットによる垂直スライス:
WRONG (horizontal):
RED: test1, test2, test3, test4, test5
GREEN: impl1, impl2, impl3, impl4, impl5
RIGHT (vertical):
RED→GREEN: test1→impl1
RED→GREEN: test2→impl2
RED→GREEN: test3→impl3
各テストは、前のサイクルで得た知見を反映します。直前にコードを書いたばかりなので、どの振る舞いが重要で、どう検証すればよいかを正確に把握できます。
トレーサーバレット
エンドツーエンドで機能することを証明するテストを、まず 1 つだけ始めます。
RED: Write test for first behavior → test fails
GREEN: Write minimal code to pass → test passes
これがトレーサーバレットです。幅広く取り組む前に、パスが機能することを証明します。その後、振る舞いを少しずつ、テストを 1 つずつ追加します。
ルール:
- 一度に 1 つのテストだけを書く
- 現在のテストを通すのに必要な分だけコードを書く
- 将来のテストを先回りしない
- テストは観測可能な振る舞いに焦点を当てる
- RED の間は絶対にリファクタリングしない — まず GREEN にする
サイクルごとのチェックリスト
[ ] Test describes behavior, not implementation
[ ] Test uses public interface only
[ ] Test would survive an internal refactor
[ ] Code is minimal for this test
[ ] No speculative features added