本章の方針
ここまでの章で「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つが複合する。
- テスト対象の依存が多すぎる:CancelSubscription が5つの依存を持っている
- テストが統合テストになっている: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 のリズムが崩れる。
根本原因
- テストが実DBに接続している
- テストが外部API(決済、メール等)を呼んでいる
setTimeout/sleepがテストに含まれている- テストデータの量が不必要に大きい
脱出法
改善手順:
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)に「△ 保留」として記録し、
必要になったときに昇格させる。
分類まとめ表
| # | アンチパターン | 症状 | 根本原因 | 関連章 |
|---|---|---|---|---|
| 01 | The Liar | テスト通過 + 本番バグ | アサーションが甘い | — |
| 02 | Excessive Setup | Arrange が20行超 | 依存過多 / 設計問題 | Ch.7, 8 |
| 03 | The Giant | 1テスト10+アサーション | 粒度が粗い | — |
| 04 | Slow Poke | スイート5分超 | DB/API直接依存 | Ch.7 |
| 05 | The Mockery | Mock 80%超 | 実装への過剰結合 | Ch.7 |
| 06 | The Inspector | private アクセス | 内部実装依存 | — |
| 07 | Second Class Citizen | テスト品質低 | テスト軽視の文化 | Ch.6 |
| 08 | The Hermit | 実行順序依存 | テスト間の状態共有 | — |
| 09 | Refactor Skipper | Green 後スキップ | 達成感/恐怖/観点不足 | Ch.6 |
| 10 | Crystal 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 を適用するのは正しくない。どこに適用し、どこに適用しないかの判断軸を提示する。