目次を表示する

TDD実践ガイド 2026

アンチパターン・カタログ ── TDD実践で踏む地雷を分類する

本章の方針

ここまでの章で「TDD の正しいやり方」を見てきた。本章では逆に、**「TDD の間違ったやり方」**を体系的に分類する。

TDD で失敗するチームのほとんどは、TDD の概念が間違っているのではなく、実践のパターンが間違っている。アンチパターンを症状から特定し、根本原因を理解し、脱出法を知っておけば、同じ地雷を踏まずに済む。

合計10個のアンチパターンを扱う。各パターンは独立して読める。


AP-01:The Liar(嘘つきテスト)

症状

テストが全て通っているのに、本番でバグが出る。テストスイートは Green なのに、ソフトウェアは壊れている。

// ❌ 嘘つきテスト:何も検証していない
test('Subscriptionを作成できる', () => {
  const sub = Subscription.startTrial('customer-1', 'plan-basic')
  expect(sub).toBeDefined()  // これは何を保証している?
})

根本原因

アサーションが甘い。「存在すること」だけを確認し、「正しいこと」を確認していない。テストを書くことが目的化し、テストの品質に意識が向いていない。

脱出法

// ✅ 具体的な値を検証する
test('トライアルとして作成するとステータスがtrialになる', () => {
  const sub = Subscription.startTrial('customer-1', 'plan-basic')
  expect(sub.status).toBe('trial')
  expect(sub.customerId).toBe('customer-1')
  expect(sub.planId).toBe('plan-basic')
})
予防ルール:
  ・expect(x).toBeDefined() は「テストの匂い」
  ・テスト名と assert の内容が一致しているか確認する
  ・「このテストが壊れたら、どんなバグを検知できるか?」と自問する

AP-02:Excessive Setup(過剰なセットアップ)

症状

テストの Arrange が20行以上ある。テストの本質(Act / Assert)よりセットアップの方が長い。テストを読んでも「何をテストしているか」がすぐに分からない。

// ❌ 過剰なセットアップ
test('キャンセルできる', async () => {
  const config = createTestConfig()
  const db = await createTestDatabase(config)
  const repo = new PrismaSubscriptionRepository(db.prisma)
  const emailService = createMockEmailService()
  const slackNotifier = createMockSlackNotifier()
  const billingService = createMockBillingService()
  const logger = createMockLogger()
  const customer = await createTestCustomer(db)
  const plan = await createTestPlan(db, { price: 1000 })
  const sub = Subscription.startTrial(customer.id, plan.id)
  sub.activate()
  await repo.save(sub)
  const useCase = new CancelSubscription(
    repo, emailService, slackNotifier, billingService, logger,
  )

  await useCase.execute(sub.id)

  expect((await repo.findById(sub.id))!.status).toBe('canceled')
})

根本原因

2つが複合する。

  1. テスト対象の依存が多すぎる:CancelSubscription が5つの依存を持っている
  2. テストが統合テストになっている:DB・メール・Slack を全て含むテストを書いている

脱出法

Ch.7-8 で学んだ技法を適用する。

// ✅ 依存を最小化し、Fake で軽量にする
test('キャンセルできる', async () => {
  const repo = new InMemorySubscriptionRepository()
  const events: DomainEvent[] = []
  const publisher: EventPublisher = {
    publish: async (e) => { events.push(e) },
  }

  const sub = Subscription.startTrial('customer-1', 'plan-basic')
  sub.activate()
  await repo.save(sub)

  await new CancelSubscription(repo, publisher).execute(sub.id)

  expect((await repo.findById(sub.id))!.status).toBe('canceled')
})
予防ルール:
  ・Arrange が10行を超えたら設計を見直すシグナル
  ・依存が3つを超えたら、責務の分離を検討する

AP-03:The Giant(巨大テスト)

症状

1つのテストに10個以上のアサーションがある。テストが失敗したとき、どのアサーションが原因か特定しにくい。

// ❌ 巨大テスト
test('Subscription のライフサイクル全体', () => {
  const sub = Subscription.startTrial('customer-1', 'plan-basic')
  expect(sub.status).toBe('trial')
  sub.activate()
  expect(sub.status).toBe('active')
  sub.changePlan('plan-premium')
  expect(sub.planId).toBe('plan-premium')
  sub.markPastDue()
  expect(sub.status).toBe('past_due')
  sub.activate()
  expect(sub.status).toBe('active')
  sub.cancel()
  expect(sub.status).toBe('canceled')
  expect(() => sub.activate()).toThrow()
})

根本原因

テストの粒度が粗い。「1テスト1振る舞い」の原則が守られていない。

脱出法

// ✅ 1テスト1振る舞い
test('トライアルからアクティベートできる', () => {
  const sub = Subscription.startTrial('customer-1', 'plan-basic')
  sub.activate()
  expect(sub.status).toBe('active')
})

test('アクティブな契約のプランを変更できる', () => {
  const sub = Subscription.startTrial('customer-1', 'plan-basic')
  sub.activate()
  sub.changePlan('plan-premium')
  expect(sub.planId).toBe('plan-premium')
})

// ... 各遷移ごとに分割
予防ルール:
  ・1テストのアサートは1〜3個を目安にする
  ・テスト名に「〜と〜と〜」のように「と」が3つ以上あったら分割する

AP-04:Slow Poke(鈍足テスト)

症状

テストスイートの実行に5分以上かかる。テストを実行する頻度が下がり、Red-Green-Refactor のリズムが崩れる。

根本原因

  1. テストが実DBに接続している
  2. テストが外部API(決済、メール等)を呼んでいる
  3. setTimeout / sleep がテストに含まれている
  4. テストデータの量が不必要に大きい

脱出法

改善手順:
  1. 最も遅いテストを特定する(vitest --reporter=verbose)
  2. DB依存 → InMemory Fake に置き換え
  3. 外部API → Stub に置き換え
  4. sleep → イベントベースの同期に変更

目安:
  ユニットテスト1本 < 10ms
  ユニットテスト全体 < 10秒
  統合テスト全体 < 2分

AP-05:The Mockery(モック乱用)

症状

テストコードの80%がモックのセットアップ。テストが「何をテストしているか」よりも「何をモックしているか」の方が目立つ。プロダクションコードを少し変えただけでテストが大量に壊れる。

// ❌ モック地獄
test('キャンセルする', () => {
  const mockRepo = vi.mocked(repo)
  mockRepo.findById.mockResolvedValue(mockSub)
  mockRepo.save.mockResolvedValue(undefined)
  const mockSub = { cancel: vi.fn(), status: 'active', id: 'sub-1' }
  // ... さらに Mock が続く
})

根本原因

Ch.7 で扱った Over-Mocking と同根。Mock が振る舞いではなく実装の詳細をテストしている。

脱出法

Mockを減らす3ステップ:
  1. Mock → Fake に置き換える(InMemory 実装)
  2. 「実装の手順」のテスト → 「結果の状態」のテストに書き換える
  3. 依存が多すぎるなら設計を見直す(責務の分離)

AP-06:The Inspector(詮索テスト)

症状

テストがプロダクションコードの private メンバーにアクセスしている。TypeScript の as any@ts-ignore がテストに登場する。

// ❌ private にアクセス
test('内部状態を検証する', () => {
  const sub = Subscription.startTrial('customer-1', 'plan-basic')
  expect((sub as any)._status).toBe('trial')       // private を覗いている
  expect((sub as any)._domainEvents).toHaveLength(0) // 内部実装に依存
})

根本原因

テストがオブジェクトの内部実装に依存している。public API だけでテストできるはずなのに、内部を覗きに行っている。

脱出法

// ✅ public API だけで検証する
test('トライアルとして生成される', () => {
  const sub = Subscription.startTrial('customer-1', 'plan-basic')
  expect(sub.status).toBe('trial')         // public getter
})

test('キャンセル時にイベントが記録される', () => {
  const sub = Subscription.startTrial('customer-1', 'plan-basic')
  sub.activate()
  sub.cancel()
  const events = sub.pullDomainEvents()     // public メソッド
  expect(events[0].type).toBe('SubscriptionCanceled')
})
予防ルール:
  ・テストに as any が登場したら設計を見直す
  ・「private を public にしたい」と思ったら、別のクラスに分離すべきサイン

AP-07:Second Class Citizen(二級市民テスト)

症状

テストコードの品質がプロダクションコードより著しく低い。命名が雑、重複だらけ、コメントなし。「テストだから」と品質を妥協している。

根本原因

「テストコードは成果物ではない」という認識。テストを「検証のための一時的なコード」と捉えている。

脱出法

テストコードの品質基準:
  ・テスト名は仕様書として読めるか
  ・重複はヘルパー関数で排除されているか
  ・Arrange-Act-Assert の構造が明確か
  ・不要になったテストは削除されているか

テストコードはプロダクションコードと同じ品質基準で管理する。
テストは「仕様の実行可能な文書」だ。

AP-08:The Hermit(隠者テスト)

症状

テストが他のテストの実行結果に依存している。テストの実行順序を変えると失敗する。単体では通るが、スイート全体では失敗する(またはその逆)。

// ❌ テスト間で状態を共有
let sharedSub: Subscription  // グローバル変数

test('作成', () => {
  sharedSub = Subscription.startTrial('customer-1', 'plan-basic')
  expect(sharedSub.status).toBe('trial')
})

test('アクティベート', () => {
  sharedSub.activate()  // 前のテストの結果に依存
  expect(sharedSub.status).toBe('active')
})

根本原因

テスト間の独立性が保たれていない。テストが共有状態を持っている。

脱出法

// ✅ 各テストが独立
test('作成するとtrialになる', () => {
  const sub = Subscription.startTrial('customer-1', 'plan-basic')
  expect(sub.status).toBe('trial')
})

test('アクティベートするとactiveになる', () => {
  const sub = Subscription.startTrial('customer-1', 'plan-basic')
  sub.activate()
  expect(sub.status).toBe('active')
})
予防ルール:
  ・テスト間で変数を共有しない
  ・vitest の --shuffle オプションでテスト順序をランダムにして確認する
  ・beforeEach で毎回新しいインスタンスを作る

AP-09:The Refactor Skipper(リファクタリング飛ばし)

症状

Red → Green は回すが、Refactor を常にスキップする。時間が経つとプロダクションコードが「動くが汚い」スパゲッティになる。テストが通っているので誰も手をつけない。

根本原因

Ch.6 で詳述した3つの原因。Green の達成感、リファクタリングの観点不足、変更への恐怖。

脱出法

強制するルール:
  ・Green になったら必ず30秒コードを眺める
  ・チェックリスト(Ch.6)を物理的に貼っておく
  ・ペアプロ/モブプロで「Refactor の帽子」を明示的に交代する

AP-10:The Crystal Ball(水晶玉テスト)

症状

「将来必要になるだろう」と予測してテストを書き、そのテストを通すために今は不要なコードを実装する。結果、使われない機能とそのテストが残る。

// ❌ 将来必要になるかもしれないテスト
test('通貨変換ができる', () => {
  const jpy = Money.of(1000, 'JPY')
  const usd = jpy.convertTo('USD', 0.0067)
  expect(usd.currency).toBe('USD')
})
// → 今は通貨変換の仕様も決まっていないのに

根本原因

YAGNI 原則の違反。「今必要なもの」ではなく「いつか必要になりそうなもの」を先回りして実装している。

脱出法

判断基準:
  「このテストが落ちたとき、今のユーザーに影響があるか?」
  → No なら、そのテストは今書くべきではない。

  テストリスト(Ch.4)に「△ 保留」として記録し、
  必要になったときに昇格させる。

分類まとめ表

#アンチパターン症状根本原因関連章
01The Liarテスト通過 + 本番バグアサーションが甘い
02Excessive SetupArrange が20行超依存過多 / 設計問題Ch.7, 8
03The Giant1テスト10+アサーション粒度が粗い
04Slow Pokeスイート5分超DB/API直接依存Ch.7
05The MockeryMock 80%超実装への過剰結合Ch.7
06The Inspectorprivate アクセス内部実装依存
07Second Class Citizenテスト品質低テスト軽視の文化Ch.6
08The Hermit実行順序依存テスト間の状態共有
09Refactor SkipperGreen 後スキップ達成感/恐怖/観点不足Ch.6
10Crystal Ball不要なテスト先行YAGNI 違反Ch.3
quadrantChart
  title TDDアンチパターンの発生頻度と影響度
  x-axis "発生頻度 低" --> "発生頻度 高"
  y-axis "影響度 低" --> "影響度 高"
  quadrant-1 "要警戒"
  quadrant-2 "最優先で対処"
  quadrant-3 "発見次第対処"
  quadrant-4 "定期チェック"
  "The Liar": [0.45, 0.85]
  "Excessive Setup": [0.70, 0.60]
  "The Giant": [0.75, 0.40]
  "Slow Poke": [0.65, 0.70]
  "The Mockery": [0.60, 0.75]
  "The Inspector": [0.35, 0.50]
  "Second Class": [0.55, 0.55]
  "The Hermit": [0.30, 0.65]
  "Refactor Skipper": [0.85, 0.80]
  "Crystal Ball": [0.40, 0.30]

本章のまとめ

要点内容
アンチパターンは10種類各パターンは症状・根本原因・脱出法の3点セット
最も危険Refactor Skipper(最も頻度が高く影響も大きい)
最も見つけにくいThe Liar(テストが通るので気づけない)
共通する対策テストの品質をプロダクションコードと同等に管理する

次章では、TDD を「使わない判断」を扱う。全てに TDD を適用するのは正しくない。どこに適用し、どこに適用しないかの判断軸を提示する。