概要
TDD(Test-Driven Development)は、構造化された開発戦略だ。
しかし現場でTDDを採用しようとしても、うまく機能しないケースが存在する。
本稿では、Java + JUnit5 をベースに、
TDDが破綻する典型パターンとその背後にある設計上の問題点を分解・解説する。
TDDが機能するには、**「正しい構造」+「適切なテスト粒度」+「思考の順序」**が必要だ。
1. パターン①:テストが書けないほど複雑なロジック
症状:
public void process() {
// if A then B, unless C, but if D then E...
// deeply nested logic
}
- 単一メソッドが多すぎる責務を抱えている
- テストを書こうとすると「どこをどう切り出せばいいか分からない」
処方:
- テストが書けない = 設計が間違っているサイン
- 責務を分離し、小さなユニットへと分割することでTDD可能に
2. パターン②:UIやDBなど副作用に密結合している
症状:
public void saveUser(User user) {
Connection con = DriverManager.getConnection(...);
...
}
- 外部I/Oに依存しており、テストのたびに副作用が発生
- モック化も困難
処方:
- 依存をインターフェースに抽象化し、注入(DI)する構造へ
- DBアクセスやAPI通信は、テスト対象外に分離
3. パターン③:テストが実装に引きずられ過ぎている
症状:
// 内部のフィールド値や呼び出し回数に依存したテスト
assertEquals(service.getInternalMap().size(), 3);
- 実装の詳細に依存し、変更に対して壊れやすい
- リファクタできなくなる
処方:
- 振る舞い(アウトプット)ベースでテストを書く
- 中身ではなく「外から見える結果」を保証する
4. パターン④:「とりあえず動かす」 → テスト後回し
症状:
- 先にコードを書き、「あとからテストを書く」方がラク
- 結果、テストは補足的になり、設計は無秩序に肥大化
処方:
- それは「TDD」ではなく「Test-After」
- TDDの本質は「先に仕様を書く」ことによる設計誘導
5. パターン⑤:テストの粒度が合っていない
症状:
- 巨大な統合テスト1本で、すべてをチェックしようとする
- 失敗時に「どこが悪いか分からない」
処方:
- ユニットレベルでの小さなテストを積み上げる
- 外部依存はモック・スタブで隔離
設計判断フロー
① テストが書きにくい? → 責務が多すぎるか、密結合している
② テストが壊れやすい? → 実装の詳細に依存している可能性
③ テストを書いても価値が薄い? → 粒度 or 仕様の曖昧さを見直す
④ 実装を先に書いていないか? → それはTDDではない
よくあるミスと対策
❌ 「TDDは面倒くさいから最後にやる」
→ ✅ TDDは後から足すものではなく、設計を導く入口である
❌ テストを書いているのに設計が良くならない
→ ✅ テストが仕様になっていない(単なる確認になっている)
❌ テストがどんどん壊れて疲弊する
→ ✅ テスト構造に依存しすぎている。リファクタに耐えうる設計を
結語
TDDが機能しないとき、問題はTDDの手法そのものではなく、設計と向き合う姿勢にある。
- テストが書けない構造こそ、改善すべき構造
- TDDは「テストを書く技術」ではなく、「仕様から設計を導く戦略」
- 書けない理由を設計の失敗として見つめ直すことが、TDD最大の価値である
TDDとは、
“書けないテストが教えてくれる、設計の限界と進化の方向である。”