tdd-anti-patterns について
テストコードのレビューやテストスイートの保守に使う、アンチパターンのリファレンスです。見つけやすい症状と原因を、関連する TDD の改善手法に結び付け、修正の優先順位付けにも役立ちます。
主な機能
- 01テストの問題と症状の一覧
- 02原因と影響の説明
- 03TDD の改善手法との対応
- 04レビューと優先順位付けの指針
活用シーン
- 01コードレビューでのテスト確認
- 02遅い CI テストや不安定なテストの整理
- 03新メンバーへのテスト規約の説明
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ yonatankarp/ software-design-skills --skill tdd-anti-patternsインストール後、Claude Code で/tdd-anti-patternsを入力して読み込まれたか確認します。
詳細
1 行での概要
テストスイートのアンチパターンをまとめたリファレンスカタログです。各アンチパターンには、見つけやすい症状、問題である理由の説明、そして解決に役立つ TDD テクニックへの案内が含まれています。
このスキルを使う場面
- テストコードのコードレビュー。
- テストスイートが見苦しくなり、何から直すべきかを判断する必要があるとき。
- オンボーディングで、「ここでは X はしない。その理由はこうです」と説明するとき。
- CI の問題(遅い、不安定、デバッグしにくい)の原因を調べるとき。
このスキルを使わない場面
- まだテストを書いていないとき — テクニックごとのスキルを直接使ってください。
- 特定のテストが正当な理由で失敗しているとき — これは監査ではなく、デバッグの対象です。
中核となる内容
テスト間の依存。 単独で実行するとパスするテストが、まとめて実行すると失敗します。症状: CI で「ローカルではいつもパスする」テストが失敗します。対策: tdd-test-isolation。
テストスイートが遅い。 1 件のテストに約 100 ms 以上かかり、テストスイート全体の実行に何時間もかかります。症状: 開発者がローカルでテストを実行しなくなります。対策: 実際の I/O、実時間、実ネットワークをテストダブルの背後に置きます(tdd-mock-object)。単体テストにはメモリ内のフェイクを使います。
不安定なテスト。 コードを変更していないのに、同じテストが失敗したりパスしたりします。症状: 「ビルドを再実行する」文化が生まれます。原因: 競合状態、実際の時計、非決定的な順序、ネットワーク。対策: 時計を注入し、エグゼキューターを注入し、乱数にシードを設定し、分離します(tdd-test-isolation)。
ミステリーゲスト。 テストが、自ら用意していないデータ(データベースの行やディスク上のファイル)に依存しています。症状: テストのアサーションに select * from users where id = 42 があり、42 がどのように用意されたのか記録がありません。対策: テスト自身でフィクスチャをセットアップします。
過剰なモック。 テストが、どのコラボレーターが呼び出されたかを検証し、どんな振る舞いが生じたかを検証していません。症状: verify(mock, times(2)) が至るところにあり、害のないリファクタリングで 10 件のテストが壊れます。対策: 境界でのみモックを使い(tdd-mock-object)、ドメイン内では実際のオブジェクトを使います。
フレームワークのテスト。 たとえば、@Transactional がロールバックすることや、Jackson が LocalDate をシリアライズすることを検証するテストです。症状: チームのコードが存在しなくてもテストはパスします。対策: 削除します。フレームワークはすでに自分自身をテストしています。
振る舞いではなく実装をテストする。 テストが、観測可能な結果ではなく、内部状態(プライベートフィールドの値やメソッドの呼び出し回数)をアサートしています。症状: 振る舞いを変えないリファクタリングで、何十件ものテストが壊れます。対策: 内部状態ではなく、出力、戻り値、外部への副作用をアサートします。
所有していないものをモックする。 テストが、サードパーティー SDK のクラスを直接モックしています。症状: SDK のアップグレードでモックの振る舞いが気付かないうちに変わり、テストはパスするのに本番環境では壊れます。対策: SDK を自分が所有する小さなインターフェースの背後にラップし、そのインターフェースをモックします。SDK 用に tdd-learning-test を追加します。
バグ修正にテストがない。 回帰テストを追加せずにバグを修正しています。症状: 数週間後にバグが再発します。対策: tdd-regression-test — バグ修正には必ずテストを追加します。
巨大なテスト。 1 件のテストで 50 行のフィクスチャをセットアップし、10 件の振る舞いを実行し、20 件の結果をアサートしています。症状: 失敗したとき、原因が誰にも分かりません。対策: 小さく、焦点を絞ったテストに分割します。原則として、1 テストにつきアサーションは 1 つにします。
コードレビューを受けていないテストコード。 テストコードが二級市民として扱われ、レビューもリファクタリングもなく、命名規則も守られていません。症状: テストスイートが本番コードよりも乱雑になります。対策: テストコードを本番コードと同じように扱います。テストスイートは、設計を実行可能な形で表した仕様です。
自分のデータクラスをモックする。 Order(...) を使えるのに mock<Order>() を使っています。症状: モックに、持っているすべてのプロパティを一つずつ設定する必要があります。対策: 実際の値オブジェクトを構築します。
ヘルパーメソッドに隠れたアサーション。 テストが verifyEverything(...) を呼び、実際のアサーションは 3 層も奥にあります。症状: 失敗メッセージが、何の説明もない「AssertionError」になります。対策: アサーションはテストメソッド内に記述します。ヘルパーは状態をセットアップするために使い、アサートはさせません。
同期のためのランダムなスリープ。 「非同期処理の完了を待つ」ために Thread.sleep(2000) を使っています。症状: テストが遅くなり、不安定にもなるのに、どちらの問題も実際には解決されません。対策: 適切な同期プリミティブを使い、フューチャーを待機し、サスペンドコードには kp-testing-coroutines の仮想時間を使います。
アサーションメッセージがない。 3 番目の引数であるメッセージを指定せずに assertEquals(expected, actual) を使っています。症状: CI の失敗には 2 つの数値が表示されるだけで、何を確認していたのか分かりません。対策: アサーションメッセージを付けるか、行ごとに説明を記述します。
判断のヒューリスティクス
- 深刻度は影響範囲に応じて変わります。CIで遅いテストがあるとすべての開発者に影響しますが、1つのファイルにある壊れやすいテスト1件なら小さな修正で済みます。
- 不安定なテストを合格するまで再実行しても、修正にはなりません。非決定性の原因を見つけてください。
- 「テストを速くするため」に追加するモックは、過剰なモック化に陥る最もよくある入り口です。新しいモックはすべて必要か検討してください。
- テストはドキュメントです。テスト名と構成から、コードの動作が分かるようにしてください。そうでなければ、合否にかかわらずテストは壊れています。
アンチパターンを見つける際のアンチパターン
- 長いテストをすべて悪いものと見なすこと。統合テストが正当に長くなる場合もあります。
- モックをゼロにするよう求めること。境界でのモックは適切であり、必要です。
- テストを再実行せずに「きれいにするため」だけにリファクタリングすること。テストコードもコードであり、同じ規律が当てはまります。
関連項目
- ほかの11個の
tdd-*スキルすべて。それぞれの修正で、関連するテクニックを参照しています。 kp-testing-coroutines— コルーチンに特有のテストの問題点。kp-anti-patterns— Kotlin言語の問題点(テストコードにもよく現れます)。
参考資料
- Kent Beck、『Test-Driven Development By Example』、第27章 — 多くのテストのアンチパターンが、パターンと併せて取り上げられています。
- Gerard Meszaros、『xUnit Test Patterns』— 網羅的なコードスメルのカタログ(標準的な参考資料)。
- Robert C. Martin、『Clean Code』、第9章 — クリーンなテスト、FIRSTの原則(Fast、Independent、Repeatable、Self-validating、Timely)。