mcpField
T
test-driven-development

test-driven-development

github.com/neolabhqテスト・品質1.8k1.3k 回インストール9 週間前に更新
リポジトリ

test-driven-development について

機能開発、バグ修正、リファクタリング、動作変更にテスト駆動開発を適用するスキルです。テストの失敗を確認してから最小限の実装を行うサイクルと、テストのアンチパターンを解説します。

主な機能

  1. 01失敗テストを先に書く開発サイクル
  2. 02Red-Green-Refactorの指針
  3. 03テスト検証のチェックポイント
  4. 04テストのアンチパターン対策

活用シーン

  1. 01新機能をテストから実装
  2. 02修正前にバグを再現
  3. 03テストを保ちながらリファクタリング

インストール

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

$npx skills add https://github.com/neolabhq/context-engineering-kit --skill test-driven-development

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

詳細

テスト駆動開発(TDD)

概要

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

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

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

使用するタイミング

常に:

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

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

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

「今回だけ TDD を省略しよう」と考えていますか? やめてください。それは自己正当化です。

絶対的なルール

NO PRODUCTION CODE WITHOUT A FAILING TEST FIRST

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

例外はありません:

  • 「参考」として残さない
  • テストを書きながら「流用」しない
  • 参照しない
  • 削除とは、削除することです

テストをもとに、まっさらな状態から実装してください。それだけです。

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

digraph tdd_cycle {
    rankdir=LR;
    red [label="RED\nWrite failing test", shape=box, style=filled, fillcolor="#ffcccc"];
    verify_red [label="Verify fails\ncorrectly", shape=diamond];
    green [label="GREEN\nMinimal code", shape=box, style=filled, fillcolor="#ccffcc"];
    verify_green [label="Verify passes\nAll green", shape=diamond];
    refactor [label="REFACTOR\nClean up", shape=box, style=filled, fillcolor="#ccccff"];
    next [label="Next", shape=ellipse];

    red -> verify_red;
    verify_red -> green [label="yes"];
    verify_red -> red [label="wrong\nfailure"];
    green -> verify_green;
    verify_green -> refactor [label="yes"];
    verify_green -> green [label="no"];
    refactor -> verify_green [label="stay\ngreen"];
    verify_green -> next;
    next -> red;
}
RED - 失敗するテストを書く

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

```typescript 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つのことだけを検証
</Good>

<Bad>
```typescript
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 を確認 - 失敗するのを見届ける

必須です。絶対に省略しないでください。

npm test path/to/test.test.ts

次の点を確認してください:

  • テストが失敗する(エラーではない)
  • 失敗メッセージが予想どおり
  • 機能が未実装であるために失敗する(誤記が原因ではない)

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

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

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

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

```typescript async function retryOperation(fn: () => Promise): Promise { for (let i = 0; i < 3; i++) { try { return await fn(); } catch (e) { if (i === 2) throw e; } } throw new Error('unreachable'); } ``` 合格に必要な分だけ ```typescript async function retryOperation( fn: () => Promise, options?: { maxRetries?: number; backoff?: 'linear' | 'exponential'; onRetry?: (attempt: number) => void; } ): Promise { // YAGNI } ``` 過剰に設計されている

機能を追加したり、ほかのコードをリファクタリングしたり、テストの範囲を超えて「改善」したりしないでください。

GREEN を確認 - テストが通るのを見届ける

必須です。

npm test path/to/test.test.ts

次の点を確認してください:

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

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

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

REFACTOR - 整理する

グリーンになった後にのみ行います:

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

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

繰り返す

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

良いテスト

品質良い例悪い例
最小限1つのこと。「and」が名前に含まれているなら、分割します。test('validates email and domain and whitespace')
明確名前が動作を説明しているtest('test1')
意図が分かる期待する API を示しているコードが何をすべきか分かりにくくしている

なぜ順序が重要なのか

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

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

  • 間違った対象をテストしているかもしれません
  • 動作ではなく、実装をテストしているかもしれません
  • 忘れていたエッジケースを見落としているかもしれません
  • バグを捕捉するところを一度も確認できません

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

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

手動テストは場当たり的です。すべてテストしたつもりでも、次のような問題があります。

  • 何をテストしたか記録が残りません
  • コードが変わったときに再実行できません
  • プレッシャーがかかると、ケースを忘れがちです
  • 「試したときは動いた」≠網羅的

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

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

サンクコストの誤謬です。その時間はすでに過ぎています。今の選択肢は次のとおりです。

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

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

「TDD は教条的です。現実的に対応するには、やり方を変えるべきです」

TDD は現実的です。

  • コミット前にバグを見つけます(あとからデバッグするより速く済みます)
  • リグレッションを防ぎます(テストで問題をすぐに検出できます)
  • 動作をドキュメント化します(テストでコードの使い方がわかります)
  • リファクタリングを可能にします(自由に変更でき、テストで問題を検出できます)

「現実的な」近道は、本番環境でのデバッグにつながり、結果的に時間がかかります。

「あとからテストしても同じ目標を達成できます。大切なのは形式ではなく精神です」

いいえ。あとから書くテストが答えるのは「これは何をするのか?」です。先に書くテストが答えるのは「これは何をすべきか?」です。

あとから書くテストは、実装に左右されます。必要なことではなく、作ったものをテストしてしまいます。発見したエッジケースではなく、覚えていたエッジケースを確認します。

先に書くテストは、実装する前にエッジケースを見つけることを促します。あとから書くテストでは、すべて覚えていたかどうかを確認します(覚えていません)。

あとから 30 分かけてテストを書いても、TDD にはなりません。カバレッジは得られても、テストが機能するという証明は得られません。

よくある言い訳

言い訳実態
「テストするほど複雑ではない」シンプルなコードも壊れます。テストは 30 秒で書けます。
「あとでテストする」テストがすぐに通っても、何も証明されません。
「あとからテストしても同じ目標を達成できる」あとから書くテスト=「これは何をするのか?」先に書くテスト=「これは何をすべきか?」
「すでに手動でテストした」場当たり的≠体系的です。記録が残らず、再実行もできません。
「X 時間分の作業を削除するのは無駄だ」サンクコストの誤謬です。検証されていないコードを残すのは技術的負債です。
「参考用に残して、先にテストを書く」そのコードに合わせてしまいます。それはあとからテストすることです。削除するなら、完全に削除してください。
「まずは調査が必要だ」問題ありません。調査用のコードは破棄し、TDD で始めてください。
「テストが難しい=設計が不明瞭だ」テストの声に耳を傾けてください。テストしにくいものは、使いにくいものです。
「TDD で作業が遅くなる」TDD はデバッグより速く済みます。現実的なやり方とは、テストファーストです。
「手動テストのほうが速い」手動テストではエッジケースを証明できません。変更のたびに再テストが必要です。
「既存のコードにテストがない」そのコードを改善しようとしています。既存のコードにもテストを追加してください。

危険信号 — 作業を止めてやり直す

  • テストより先にコードを書く
  • 実装後にテストする
  • テストがすぐに通る
  • テストが失敗した理由を説明できない
  • テストを「あとで」追加する
  • 「今回だけ」と正当化する
  • 「すでに手動でテストした」
  • 「あとからテストしても同じ目的を達成できる」
  • 「大切なのは形式ではなく精神だ」
  • 「参考用に残す」または「既存のコードを流用する」
  • 「すでに X 時間かけたので、削除するのは無駄だ」
  • 「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

リファクタリング 必要に応じて、複数のフィールドに対する検証を切り出します。

検証チェックリスト

作業を完了としてマークする前に、以下を確認してください。

  • 新しい関数/メソッドごとにテストがある
  • 実装前に、各テストが失敗することを確認した
  • 各テストは想定どおりの理由で失敗した(機能が未実装であり、タイプミスではない)
  • 各テストを通すための最小限のコードを書いた
  • すべてのテストが成功する
  • 出力がクリーンである(エラーや警告がない)
  • テストでは実際のコードを使用している(やむを得ない場合に限りモックを使用)
  • エッジケースとエラーを網羅している

すべての項目にチェックを付けられませんか?TDD を飛ばしています。最初からやり直してください。

行き詰まったとき

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

統合のデバッグ

バグが見つかりましたか?再現する失敗テストを書きます。TDD のサイクルに従います。テストで修正を検証し、再発を防ぎます。

テストなしでバグを修正してはいけません。

最終ルール

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

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


テストのアンチパターン

概要

テストではモックの動作ではなく、実際の動作を検証する必要があります。モックはテスト対象そのものではなく、分離のための手段です。

**基本原則:**モックの動作ではなく、コードの動作をテストします。

厳格な TDD に従えば、こうしたアンチパターンを防止できます。

鉄則

1. NEVER test mock behavior
2. NEVER add test-only methods to production classes
3. NEVER mock without understanding dependencies

アンチパターン 1:モックの動作をテストする

違反例:

// ❌ BAD: Testing that the mock exists
test('renders sidebar', () => {
  render(<Page />);
  expect(screen.getByTestId('sidebar-mock')).toBeInTheDocument();
});

これが誤りである理由:

  • コンポーネントが機能するかではなく、モックが機能するかを検証している
  • モックがある場合はテストに合格し、ない場合は失敗する
  • 実際の動作について何もわからない

人間のパートナーからの指摘:「モックの動作をテストしているのではありませんか?」

修正方法:

// ✅ GOOD: Test real component or don't mock it
test('renders sidebar', () => {
  render(<Page />);  // Don't mock sidebar
  expect(screen.getByRole('navigation')).toBeInTheDocument();
});

// OR if sidebar must be mocked for isolation:
// Don't assert on the mock - test Page's behavior with sidebar present
ゲート関数
BEFORE asserting on any mock element:
  Ask: "Am I testing real component behavior or just mock existence?"

  IF testing mock existence:
    STOP - Delete the assertion or unmock the component

  Test real behavior instead

アンチパターン 2:本番コードにテスト専用メソッドを追加する

違反例:

// ❌ BAD: destroy() only used in tests
class Session {
  async destroy() {  // Looks like production API!
    await this._workspaceManager?.destroyWorkspace(this.id);
    // ... cleanup
  }
}

// In tests
afterEach(() => session.destroy());

これが誤りである理由:

  • 本番クラスがテスト専用コードで汚染される
  • 誤って本番環境で呼び出された場合に危険
  • YAGNI と関心の分離に反する
  • オブジェクトのライフサイクルとエンティティのライフサイクルを混同する

修正方法:

// ✅ GOOD: Test utilities handle test cleanup
// Session has no destroy() - it's stateless in production

// In test-utils/
export async function cleanupSession(session: Session) {
  const workspace = session.getWorkspaceInfo();
  if (workspace) {
    await workspaceManager.destroyWorkspace(workspace.id);
  }
}

// In tests
afterEach(() => cleanupSession(session));
ゲート関数
BEFORE adding any method to production class:
  Ask: "Is this only used by tests?"

  IF yes:
    STOP - Don't add it
    Put it in test utilities instead

  Ask: "Does this class own this resource's lifecycle?"

  IF no:
    STOP - Wrong class for this method

アンチパターン 3:理解せずにモックを使う

違反例:

// ❌ BAD: Mock breaks test logic
test('detects duplicate server', () => {
  // Mock prevents config write that test depends on!
  vi.mock('ToolCatalog', () => ({
    discoverAndCacheTools: vi.fn().mockResolvedValue(undefined)
  }));

  await addServer(config);
  await addServer(config);  // Should throw - but won't!
});

これが誤りである理由:

  • モックしたメソッドには、テストが依存する副作用(設定の書き込み)があった
  • 「念のため」と過剰にモックすると、実際の動作が壊れる
  • 誤った理由でテストに合格するか、原因不明の失敗が起きる

修正方法:

// ✅ GOOD: Mock at correct level
test('detects duplicate server', () => {
  // Mock the slow part, preserve behavior test needs
  vi.mock('MCPServerManager'); // Just mock slow server startup

  await addServer(config);  // Config written
  await addServer(config);  // Duplicate detected ✓
});
ゲート関数
BEFORE mocking any method:
  STOP - Don't mock yet

  1. Ask: "What side effects does the real method have?"
  2. Ask: "Does this test depend on any of those side effects?"
  3. Ask: "Do I fully understand what this test needs?"

  IF depends on side effects:
    Mock at lower level (the actual slow/external operation)
    OR use test doubles that preserve necessary behavior
    NOT the high-level method the test depends on

  IF unsure what test depends on:
    Run test with real implementation FIRST
    Observe what actually needs to happen
    THEN add minimal mocking at the right level

  Red flags:
    - "I'll mock this to be safe"
    - "This might be slow, better mock it"
    - Mocking without understanding the dependency chain

アンチパターン 4:不完全なモック

違反例:

// ❌ BAD: Partial mock - only fields you think you need
const mockResponse = {
  status: 'success',
  data: { userId: '123', name: 'Alice' }
  // Missing: metadata that downstream code uses
};

// Later: breaks when code accesses response.metadata.requestId

これが誤りである理由:

  • 部分的なモックは構造上の前提を隠します - 把握しているフィールドしかモックしていません
  • 後続のコードが、含めていないフィールドに依存する可能性があります - 気付かれないまま失敗します
  • テストは通るのに統合時に失敗します - モックは不完全ですが、実際の API は完全です
  • 誤った安心感を与えます - テストは実際の動作について何も証明していません

**鉄則:**直近のテストで使うフィールドだけでなく、実際に存在する COMPLETE なデータ構造をモックしてください。

修正方法:

// ✅ GOOD: Mirror real API completeness
const mockResponse = {
  status: 'success',
  data: { userId: '123', name: 'Alice' },
  metadata: { requestId: 'req-789', timestamp: 1234567890 }
  // All fields real API returns
};
ゲート関数
BEFORE creating mock responses:
  Check: "What fields does the real API response contain?"

  Actions:
    1. Examine actual API response from docs/examples
    2. Include ALL fields system might consume downstream
    3. Verify mock matches real response schema completely

  Critical:
    If you're creating a mock, you must understand the ENTIRE structure
    Partial mocks fail silently when code depends on omitted fields

  If uncertain: Include all documented fields

アンチパターン 5:統合テストを後回しにする

違反例:

✅ Implementation complete
❌ No tests written
"Ready for testing"

これが誤りである理由:

  • テストは実装の一部であり、任意の後続作業ではありません
  • TDD ならこれを検出できていたはずです
  • テストなしでは完了したと言えません

修正方法:

TDD cycle:
1. Write failing test
2. Implement to pass
3. Refactor
4. THEN claim complete

モックが複雑になりすぎたとき

警告サイン:

  • モックのセットアップがテストロジックより長い
  • テストを通すためにあらゆるものをモックしている
  • モックに、実際のコンポーネントが持つメソッドが欠けている
  • モックを変更するとテストが壊れる

人間のパートナーからの質問:「ここでモックを使う必要がありますか?」

**検討事項:**実際のコンポーネントを使う統合テストのほうが、複雑なモックより簡単な場合がよくあります

TDD で防げるアンチパターン

TDD が役立つ理由:

  1. 最初にテストを書く → 実際に何をテストするのか考えざるを得なくなります
  2. テストが失敗することを確認する → モックではなく、実際の動作をテストしていることを確認できます
  3. 最小限の実装にする → テスト専用のメソッドが紛れ込まなくなります
  4. 実際の依存関係を使う → モックする前に、テストで本当に必要なものがわかります

モックの動作をテストしているなら、TDD に違反しています ― 実際のコードでテストが失敗することを確認する前に、モックを追加したからです。

クイックリファレンス

アンチパターン修正方法
モック要素に対してアサーションする実際のコンポーネントをテストするか、モックを解除します
本番コードにテスト専用のメソッドがあるテスト用ユーティリティに移動します
理解せずにモックするまず依存関係を理解し、必要最小限のモックを使います
不完全なモック実際の API を完全に再現します
後回しにされるテストTDD ― 先にテストを書きます
複雑すぎるモック統合テストを検討します

注意すべき兆候

  • アサーションで *-mock テスト ID を確認している
  • テストファイルでしか呼び出されないメソッドがある
  • テストの 50% 超がモックのセットアップ
  • モックを削除するとテストが失敗する
  • なぜモックが必要なのか説明できない
  • 「念のため」にモックしている

要点

モックは分離するためのツールであり、テスト対象ではありません。

TDD によってモックの動作をテストしているとわかったなら、誤った方向に進んでいます。

修正方法:実際の動作をテストするか、そもそもなぜモックしているのかを問い直します。

コメント