mcpField
T
test-driven-development

test-driven-development

github.com/romiluz13テスト・品質16636 回インストール1 週間前に更新
リポジトリ

test-driven-development について

ソフトウェア開発向けの TDD スキルで、最小限の実装とリファクタリングの前に、テストを書いて失敗を確認することを求めます。段階的なサイクルやテスト実行の規律、検証チェックリストでテストファーストの作業を整えます。

主な機能

  1. 01Red-Green-Refactor サイクル
  2. 02振る舞いテストを一つずつ進行
  3. 03実装前に失敗を確認
  4. 04テスト実行とプロセス終了の規則
  5. 05テスト設計とモックのガイド
  6. 06カバレッジと完了チェックリスト

活用シーン

  1. 01新機能の実装前にテストを作成
  2. 02不具合修正後に回帰動作を確認
  3. 03テストを維持しながらコードをリファクタリング

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/romiluz13/cc10x --skill test-driven-development

インストール後、Claude Code で/test-driven-developmentを入力して読み込まれたか確認します。

同梱ファイル

SKILL.md と一緒にインストールされる参考資料やスクリプト。

references/
integration-and-live-proof.md
test-data-and-mocks.md
testing-patterns.md

詳細

テスト駆動開発(TDD)

概要

まずテストを書きます。失敗することを確認します。テストを通すための最小限のコードを書きます。

基本原則: テストが失敗するのを確認していないなら、そのテストが正しいものを検証しているかどうかは分かりません。

ルールの文言に違反することは、ルールの精神に違反することです。

参照ファイル

現在のテストサイクルに必要な参照ファイルだけを読みます。

  • references/testing-patterns.md:命名、AAA 構造、僅差の否定ケース、動作への着目、アンチパターンの確認
  • references/test-data-and-mocks.md:ファクトリー、モックの境界、一般的な境界モック、環境変数と時刻の扱い
  • references/integration-and-live-proof.md:単体テストだけでは不十分な場合や、実際の API、シードデータ、ブラウザ操作、負荷検証が計画に含まれる場合

使用するタイミング

常に使用:

  • 新機能
  • バグ修正
  • リファクタリング
  • 動作の変更

例外(人間のパートナーに確認してください):

  • 使い捨てのプロトタイプ
  • 生成コード
  • 設定ファイル

「今回だけ TDD を省略してもいいのでは?」と思ったら、立ち止まってください。それは正当化です。

鉄則

NO PRODUCTION CODE WITHOUT A FAILING TEST FIRST

テストより先にコードを書きましたか? 削除して、最初からやり直してください。

例外はありません:

  • 「参考用」として残さないでください
  • テストを書きながら「調整」しないでください
  • 見ないでください
  • 削除とは、完全に削除することです

テストをもとに、新たに実装してください。それだけです。

テストプロセスの規律(重要)

問題: テストランナー(Vitest、Jest)はデフォルトでウォッチモードになり、プロセスがいつまでも終了しないことがあります。

必須ルール:

  1. 常に実行モードを使ってください — ウォッチモードは決して起動しないでください。
    • Vitest:npx vitest run(npx vitestではありません)
    • Jest:CI=true npx jestまたはnpx jest --watchAll=false
    • npm スクリプト:CI=true npm testまたはnpm test -- --run
  2. すべてのテストコマンドには、CI=true のプレフィックスを付けることを推奨します: CI=true npm test
  3. TDD サイクルの完了後、孤立したプロセスがないことを確認してください: pgrep -f "vitest|jest" || echo "Clean"
  4. 見つかったら終了してください: pkill -f "vitest" 2>/dev/null || true

レッド・グリーン・リファクタリング

    ┌─────────┐       ┌─────────┐       ┌───────────┐
    │   RED   │──────>│  GREEN  │──────>│ REFACTOR  │
    │ (Fail)  │       │ (Pass)  │       │ (Clean)   │
    └─────────┘       └─────────┘       └───────────┘
         ^                                    │
         │                                    │
         └────────────────────────────────────┘
                    Next Feature
垂直スライス(重要)
WRONG (horizontal — all tests then all code):
  RED:   test1, test2, test3, test4, test5
  GREEN: impl1, impl2, impl3, impl4, impl5

RIGHT (vertical — one feature at a time):
  RED->GREEN: test1->impl1
  RED->GREEN: test2->impl2
  RED->GREEN: test3->impl3

すべてのテストを先に書き、その後ですべてを実装するのは避けてください。 この方法では、質の悪いテストが生まれます。

  • まとめて書いたテストは、実際の動作ではなく、_想像上の_動作を検証します
  • ユーザーに見える動作ではなく、物事の_形_をテストすることになります
  • テストが実際の変更に反応しなくなり、動作が壊れても通り、問題がなくても失敗するようになります
  • 実装を理解する前にテスト構造を決めてしまい、見通しを誤ります

正しい方法: 1 つのテスト → 1 つの実装 → 繰り返す。各テストは、前のサイクルで得た知見を踏まえて作成します。

RED - 失敗するテストを書く

期待する動作を示す、最小限のテストを 1 つ書きます。

良い例:

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 つ検証しています

悪い例:

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 を確認 - 失敗する様子を見る

必須です。決して省略しないでください。

CI=true npm test path/to/test.test.ts

次を確認してください:

  • テストが失敗する(エラーではない)
  • 失敗メッセージが想定どおりである
  • 機能が未実装であるために失敗している(タイプミスが原因ではない)

テストが通りましたか? 既存の動作をテストしています。テストを修正してください。

テストでエラーが発生しましたか? エラーを修正し、正しく失敗するまで再実行してください。

GREEN - 最小限のコードを書く

テストを通すための最もシンプルなコードを書きます。

良い例:

async function retryOperation<T>(fn: () => Promise<T>): Promise<T> {
  for (let i = 0; i < 3; i++) {
    try {
      return await fn();
    } catch (e) {
      if (i === 2) throw e;
    }
  }
  throw new Error('unreachable');
}

テストを通すために必要な分だけです

悪い例:

async function retryOperation<T>(
  fn: () => Promise<T>,
  options?: {
    maxRetries?: number;
    backoff?: 'linear' | 'exponential';
    onRetry?: (attempt: number) => void;
  }
): Promise<T> {
  // YAGNI - You Ain't Gonna Need It
}

過剰な設計です

機能を追加したり、別のコードをリファクタリングしたり、テストの範囲を超えて「改善」したりしないでください。テストの値をハードコードしないでください。すべての入力で機能する汎用的なロジックを実装してください。

GREEN を確認 - テストが通る様子を見る

必須です。

CI=true npm test path/to/test.test.ts

次を確認してください:

  • テストが通る
  • ほかのテストも引き続き通る
  • 出力に問題がない(エラーや警告がない)

テストが失敗しましたか? テストではなくコードを修正してください。

ほかのテストが失敗しましたか? すぐに修正してください。

REFACTOR - 整理する

グリーンになった後だけ、次の作業を行います。

  • 重複を取り除く
  • 名前を改善する
  • ヘルパーを抽出する

テストが通る状態を保ってください。動作は追加しないでください。

繰り返す

次の機能について、次の失敗するテストを書きます。

良いテスト

品質良い例悪い例
最小限1つのことだけをテストします。名前に「and」が含まれている場合は分割します。test('validates email and domain and whitespace')
明確名前が動作を説明していますtest('test1')
意図を示す望ましい API の使い方を示しますコードがどう動くべきかを分かりにくくします

テスト構造の詳細、境界値に近い負のテスト、動作と内部実装の違い、コードの臭いのチェックについては、references/testing-patterns.mdをお読みください。

ファクトリー、モック、環境変数や時間の扱いについては、 references/test-data-and-mocks.mdをお読みください。

順序が重要な理由

「動作確認のため、あとでテストを書きます」

コードを書いたあとに作成したテストは、すぐに通ります。すぐに通ることは何も証明しません。

  • 間違ったものをテストしている可能性があります
  • 動作ではなく、実装をテストしている可能性があります
  • 忘れていたエッジケースを見落とす可能性があります
  • テストがバグを検出する様子を見たことがありません

テストファーストならテストが失敗するのを確認でき、実際に何かをテストしていると証明できます。

「エッジケースはすべて手動でテストしました」

手動テストは場当たり的です。すべてテストしたと思っていても、次の問題があります。

  • 何をテストしたか記録がありません
  • コード変更後に再実行できません
  • プレッシャーの中でケースを忘れやすくなります
  • 「試したときは動いた」≠ 網羅的

自動テストは体系的です。毎回同じ方法で実行されます。

「X 時間分の作業を削除するのは無駄です」

サンクコストの誤謬です。費やした時間はすでに戻りません。今の選択肢は次のとおりです。

  • 削除して TDD で書き直す(さらに X 時間、高い信頼性)
  • そのままにしてあとからテストを追加する(30 分、信頼性が低く、バグが発生しやすい)

本当の「無駄」は、信頼できないコードを残すことです。実際のテストがない動作するコードは技術的負債です。

危険信号 - STOPして最初からやり直す

次のようなことに気づいたら、

  • テストより先にコードを書く
  • 実装後にテストする
  • テストがすぐに通る
  • テストが失敗した理由を説明できない
  • テストの追加を「あとで」にする
  • 「今回だけ」と正当化する
  • 「すでに手動でテストした」
  • 「あとからテストしても同じ目的を達成できる」
  • 「大切なのは形式ではなく精神だ」
  • 「参考として残す」または「既存のコードを適応させる」
  • 「すでに X 時間費やしたから、削除するのは無駄だ」
  • 「TDD は教条的すぎる。私は現実的に対応している」
  • 「これは違う。なぜなら……」

これらはすべて、コードを削除してTDDで最初からやり直すべきという意味です。

正当化を防ぐ

言い訳実態
「テストするには単純すぎる」単純なコードも壊れます。テストは30秒で書けます。
「あとでテストする」すぐに通るテストは何も証明しません。
「あとからテストしても同じ目的を達成できる」テストをあとから書く=「これは何をするのか?」。テストファースト=「これは何をすべきか?」。
「すでに手動でテストした」場当たり的 ≠ 体系的。記録がなく、再実行もできません。
「X 時間分の作業を削除するのは無駄だ」サンクコストの誤謬です。検証されていないコードを残すことは技術的負債です。
「参考として残し、テストを先に書く」それをもとに手を加えるでしょう。それはあとからテストすることです。削除とは削除することです。
「まず調査する必要がある」かまいません。調査用のコードは捨て、TDDで始めてください。
「テストが難しい=設計が不明確」テストから学びましょう。テストしにくいものは使いにくいものです。
「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 必要であれば、複数のフィールドの検証を切り出します。

検証チェックリスト

作業を完了とする前に確認してください。

  • 新しい関数/メソッドすべてにテストがある
  • 実装前に各テストが失敗するのを確認した
  • 各テストは想定どおりの理由で失敗した(タイプミスではなく、機能がないため)
  • 各テストを通すための最小限のコードを書いた
  • すべてのテストが通る
  • 出力に問題がない(エラーや警告がない)
  • テストでは実際のコードを使用している(避けられない場合のみモックを使用)
  • エッジケースとエラーを網羅している
  • ハングしたテストプロセスがない(pgrep -f "vitest|jest" の結果が空)

すべての項目にチェックできませんか? TDDをスキップしています。最初からやり直してください。

統合と実稼働環境での検証

承認された計画やリスクプロファイルがローカル環境での動作を超える場合は、 references/integration-and-live-proof.mdを読んでください。

次の項目に依存するタスクでは、ユニットテストだけでは不十分です。

  • 実際のAPI呼び出し
  • データのシードまたはリセット
  • ブラウザーまたはワーカーのオーケストレーション
  • サービスをまたぐ副作用
  • 負荷またはストレス時の動作

このような場合は、内部の反復サイクルではTDDを維持し、外部の検証では検証の深度を高めてください。

カバレッジのしきい値(プロジェクトのデフォルト)

目標:コードカバレッジ80%以上。対象は以下のとおりです。

  • 分岐:80%
  • 関数:80%
  • 行:80%
  • ステートメント:80%

確認方法: npm run test:coverageまたは同等のコマンドを使用します。

しきい値未満の場合は? 完了を宣言する前に、不足しているテストを追加してください。

テストの優先順位付け

カバレッジ80%を達成するには、どのテストを優先するかを意識して選ぶ必要があります。次の項目に注力してください。

  • ユーザー向けの重要な経路(認証、決済、データの整合性)
  • 分岐が複数ある複雑なロジック
  • これまでに不具合が発生したことのあるコード(回帰が起きやすい箇所)

コードが「単純に見える」からといって、テストを省略しないでください。単純なコードでも壊れます。80%という目標は最低ラインであり、上限ではありません。

行き詰まったとき

問題解決策
テスト方法がわからない望ましいAPIを書きます。先にアサーションを書きます。人間のパートナーに相談します。
テストが複雑すぎる設計が複雑すぎます。インターフェースをシンプルにします。
すべてをモックする必要があるコードの結合度が高すぎます。依存性注入を使います。
テストのセットアップが大きすぎるヘルパーを切り出します。それでも複雑なら、設計をシンプルにします。

テスト容易性を考慮した設計(テストが難しい場合)

テストを書くのが難しい場合は、インターフェースを見直す必要があります。

  1. 依存関係を受け取る。自分で作成しない

    • テストしやすい例:function processOrder(order, paymentGateway) {}
    • テストしにくい例:function processOrder(order) { const gw = new StripeGateway(); }
  2. 副作用を発生させず、結果を返す

    • テストしやすい例:function calculateDiscount(cart): Discount {}
    • テストしにくい例:function applyDiscount(cart): void { cart.total -= discount; }
  3. 表面積を小さくする — メソッドが少ないほど必要なテストが減り、パラメーターが少ないほどセットアップがシンプルになります。

振る舞いに焦点を当てる

オブジェクトの中身ではなく、オブジェクトがどのように連携するかをテストしてください。テストが.state、.length、またはプライベートフィールドを調べているなら、それは構造をテストしています。内部の動作が変わっても振る舞いが変わらない場合に、そのテストは失敗します。

テスト対象正しい例誤った例
関数の出力expect(calculate(input)).toBe(result)expect(calculator.internalCache).toContain(...)
コンポーネントの振る舞いexpect(screen.getByText('Saved')).toBeTruthy()expect(component.state.saved).toBe(true)
サービスとの連携expect(response.status).toBe(201)expect(service.callCount).toBe(1)

これはすでに、Test Smells表の「実装のテスト」という問題点で示されています。これを基本的な視点にしてください。すべてのアサーションは「ユーザー/呼び出し元が何を観察したか」に答えるべきであり、「内部で何が起きたか」に答えるべきではありません。

エージェント間のテスト契約

CC10xが複数のエージェント間で作業を割り振る場合(プランナーがテスト仕様を書き、ビルダーが実装し、レビュアーが検証する場合)、テストファイルが契約そのものです。

  • プランナーは、計画内のテスト名とアサーションで期待される振る舞いを定義します
  • ビルダーは先にテストを書き、すべて成功するように実装します。テストファイルが契約の達成を証明します
  • レビュアーは同じテストを再実行します。合格なら契約が履行され、不合格なら契約が破られています

契約の内容を文章で重複して書かないでください。テストファイルが要件を表しているなら、テストファイルが要件です。

出力形式

## TDD Cycle

### Requirements
[What functionality is being built]

### RED Phase
- Test: [test name]
- Command: `npm test -- --grep "test name"`
- Result: exit 1 (FAIL as expected)
- Failure reason: [function not defined / expected X got Y]

### GREEN Phase
- Implementation: [summary]
- File: [path:line]
- Command: `npm test -- --grep "test name"`
- Result: exit 0 (PASS)

### REFACTOR Phase
- Changes: [what was improved]
- Command: `npm test`
- Result: exit 0 (all tests pass)

最終ルール

Production code → test exists and failed first
Otherwise → not TDD

人間のパートナーの許可がない限り、例外は認められません。

コメント