unit-test-hygiene
既存のテストスイートを棚卸しし、不要・重複・冗長・外部依存の混入を整理するためのスキルです。
このスキルの仕事は、単に「モックを増やす」ことではありません。価値があるのは、どのテストを残し、どれを削り、どれを統合し、どこに境界を引くべきかを説明可能な形で整理することです。
このスキルが扱うこと
- 古い仕様・廃止機能にぶら下がった不要テストの発見
- 重複・冗長・過剰に実装詳細へ依存したテストの整理
- API / URL / timer / clock / command / DB / filesystem / env などの外部依存を直接利用しているテストの検出
- unit test と integration test の境界の見直し
- mock / stub / fake / dependency injection の方針提案
- flaky になりやすいテストのシグナル抽出
基本姿勢
- まず調査し、分類し、根拠を示す。
- いきなり削除しない。削除候補として扱い、なぜ削除してよいのかを説明する。
- いきなり「全部モック化」しない。unit test では隔離し、integration test や contract test では残す価値を判断する。
- 実装詳細の一致より、振る舞いの価値と保守コストを見る。
- テストの数ではなく、信頼性・意図の明瞭さ・変更耐性を改善する。
ワークフロー
1. テストスイートの棚卸し
最初に以下を把握する。
- どのテストフレームワークを使っているか
- どこに test / spec / integration / e2e があるか
- 主要な外部依存は何か
- deprecated / legacy / obsolete / unused を示すシグナルがどこにあるか
- 本番コード側に、依存注入や adapter 境界がすでにあるか
必要に応じて以下を読む。
README.mddocs/- テスト設定ファイル(例: jest, vitest, pytest, go test, cargo test 周辺)
- 主要な test ディレクトリ
- 外部依存を持つ production code
2. テストを分類する
各テスト、またはテスト群を次の観点で分類する。
A. 生存性
- 現役: 現在の仕様・公開 API・現在の振る舞いを守っている
- 要確認: 古い仕様の名残かもしれないが、即削除は危険
- 削除候補: すでに存在しない機能や旧仕様のみを固定している
B. 重複度
- 明確な重複: 同じ振る舞いをほぼ同じ入力・期待値で検証している
- 部分重複: セットアップや期待値の大半が共通
- 統合候補: parameterized test や shared helper でまとめられる
C. unit 純度
- pure unit: 外部依存が隔離され、決定的
- hidden integration: unit を名乗るが外部依存や環境状態に触れている
- integration 相当: 実際には統合テストとして扱うべき
D. 外部依存
最低限、次を外部依存候補として扱う。
- HTTP / API / URL / RPC
- DB / queue / cache
- clock / timer / sleep / timeout / retry
- subprocess / shell / command
- filesystem
- environment variable / process-global state
- random / UUID / nondeterministic source
E. 安定性リスク
- flaky
- timing-sensitive
- order-dependent
- environment-dependent
- network-dependent
3. 問題の根拠を集める
発見ごとに、次のいずれかの根拠を必ず添える。
- 現行コードに対象機能が見当たらない
- docs / README / public API とズレている
- 同じ振る舞いを別テストがすでにカバーしている
- 単一の内部実装に過剰結合している
- テストが直接外部依存を叩いている
- sleep / now / random / env / shared state に依存している
根拠が弱い場合は断定せず、要確認 に落とす。
4. 改善方針を出す
各発見に対して、次のどれが妥当かを選ぶ。
- 削除
- 統合
- parameterized test 化
- shared helper 抽出
- mock 化
- fake 化
- stub 化
- dependency injection 導入
- integration test へ移管
- contract test として明示的に残す
削除候補にしやすいケース
次の条件が複数当てはまるほど、削除候補として強い。
- 対象機能が production code から消えている
- 公開 API / CLI / route / export に存在しない
- docs や README に現行機能として現れない
- 旧エラーメッセージや旧内部関数名だけを固定している
- 別テストが同じ振る舞いをより高い粒度で守っている
- 長期放置された skip / todo / deprecated テストである
ただし、互換性維持のために残している可能性があるなら、その前提を明記して 要確認 にする。
重複判定の軸
重複は単なる行数の近さではなく、次の 4 軸で見る。
- セットアップ
- 入力
- 期待値
- 守ろうとしている振る舞い
4 軸のうち 3 つ以上が実質同じなら、重複候補として扱ってよい。
モックの判断原則
モック・フェイクを勧めるケース
- unit test がネットワークへ出る
- unit test が DB を直接叩く
- unit test が現在時刻や sleep に依存する
- unit test が subprocess を起動する
- unit test が process-global state に依存する
モックしない方がよいケース
- adapter / repository / HTTP client の integration test
- migration test
- CLI の smoke test
- 契約検証が主目的の contract test
この場合は「unit test からは分離し、integration test として明示する」方向を優先する。
言語別の具体シグナル
SKILL.md は言語非依存で進める。具体的な探索シグナルやモック手法が必要なときは、該当言語の reference を読む。
- JavaScript / TypeScript:
unit-test-hygiene/references/javascript-typescript.md - Python:
unit-test-hygiene/references/python.md - Go:
unit-test-hygiene/references/go.md - Rust:
unit-test-hygiene/references/rust.md - 共通ヒューリスティクス:
unit-test-hygiene/references/heuristics.md - mock / stub / fake の使い分け:
unit-test-hygiene/references/mocking-playbook.md
出力フォーマット
以下の構造を基本に、日本語で返す。
## テスト棚卸しサマリ
- 総評: ...
- 不要テスト候補: ...
- 重複/冗長候補: ...
- 外部依存混入候補: ...
- flaky リスク候補: ...
## 発見事項
### 1) 不要・旧仕様テスト
- 対象: `path/to/test`
- 判定: 削除候補 / 要確認
- 根拠: ...
- 推奨アクション: ...
### 2) 重複・冗長テスト
- 対象: ...
- 重複内容: ...
- 推奨アクション: ...
### 3) 外部依存の直接利用
- 対象: ...
- 依存種別: API / URL / timer / command / DB / filesystem / env
- 問題: ...
- 推奨境界: mock / fake / DI / integrationへ移管
### 4) flaky / 不安定要因
- 対象: ...
- 不安定要因: ...
- 推奨アクション: ...
## 優先度付き整理計画
- P0: ...
- P1: ...
- P2: ...
- P3: ...
## モック/分離設計メモ
- どこを境界にするか
- どこは fake が良いか
- どこは integration test として残すべきか
よい出力の条件
- ファイルやテスト名を具体的に挙げる
- 推測と事実を分ける
- 「削除」「統合」「モック」「移管」を混同しない
- 根拠なしで obsolete と断定しない
- すぐ直すべき順番がわかるように優先度をつける
- 実装者が次の作業を issue に切り出せる粒度にする
避けること
- テストは多いほど良い、という前提で話す
- すべての外部依存を機械的にモック対象にする
- integration test の価値を無視する
- 実装詳細 assertions の削減だけを目的化する
- 断片的な grep だけで大胆な削除提案をする