name: retrofit description: テストが少ない・ない既存システムへのテスト追加ワークフロー。Characterization Test・Seam の導入・TDD への移行を段階的に進める。引数にファイルパスを渡すと対象ファイルへのテスト追加から直接開始できる。
/retrofit
既存コードへのテスト後付けワークフロー。 Feathers "Working Effectively with Legacy Code" のアプローチに基づく。
PHASE 1: Survey — 現状を計測・把握する
PHASE 2: Pinpoint — どこから始めるか決める
PHASE 3: Characterize — 現在の挙動を固定する(セーフティネット)
PHASE 4: Seam — テスト可能な構造に切り出す
PHASE 5: TDD Forward — 以降の変更をテストファーストにする
NOTE: Phase 3 が Phase 4 より先が原則。「テストなしに構造を変えるな」がFeathersの原則。 ただし Phase 3 自体が不可能なターゲット(後述)は例外として Phase 4 を先に適用する。
起動方法
/retrofit → PHASE 1 から開始(コードベース全体を調査して優先順位をつける)
/retrofit <filepath> → 指定ファイルへのテスト追加を直接開始
IF ファイルが存在しない: エラーを報告して停止する
対象リスト = [filepath] として PHASE 3 へスキップする
※ PHASE 1・2 は省略される
PHASE 1: Survey(現状把握)
目的: テストが「どこにあって・どこにない・何が危険か」を数値で把握する。
STEP 1: テストランナーとカバレッジツールの確認
IF テスト環境が設定済み:
RUN: カバレッジ計測コマンドを実行する
REPORT: カバレッジ率・未カバーファイルの一覧
ELSE:
PROPOSE: 言語に応じた推奨ツールを提示する(ユーザーが選択)
Node.js : vitest --coverage(v8)または jest --coverage
Python : pytest --cov
Go : go test -cover
Java : JaCoCo
Ruby : simplecov
その他 : その言語の標準的なカバレッジツールを提案する
WAIT_FOR: ユーザーの選択・セットアップ完了
IF セットアップをスキップ(後でやる / 不要):
NOTE: カバレッジ計測なしで STEP 2 へ進む。
PHASE 2 のスコアリングは変更頻度・複雑度のみで行う。
ENDIF
ENDIF
STEP 2: 変更頻度の高いファイルを特定する
IF git が使える:
RUN: git log --format=format: --name-only | sort | uniq -c | sort -rn | head -20
ELSE:
ASK USER: 「よく変更するファイル・モジュールを教えてください」
ENDIF
STEP 3: 複雑度・依存の多いファイルを目視で特定する
CHECK: 行数が長い・ネストが深い・他モジュールへの依存が多い箇所
GATE: Survey 結果をユーザーに提示し、内容を確認してから次へ進む PROHIBITED: ユーザー確認なしに PHASE 2 へ進むこと
PHASE 2: Pinpoint(優先順位付け)
目的: 限られた時間でROIが最大になる場所から始める。
以下の基準でスコアリングして優先対象を絞る(加点が高い順に着手):
| 基準 | 加点 |
|---|---|
| バグ履歴がある(git log / issue で確認) | +3 |
| 変更頻度が高い(PHASE 1 で上位) | +3 |
| 認証・決済・DB書き込みなど高リスクな処理を含む | +2 |
| テストゼロ | +2 |
| 複雑度が高い(条件分岐・ネストが多い) | +1 |
SHOW USER: スコアリング結果と推奨着手順を提示する
ASK USER: 「この優先順位で進めますか?変更があれば教えてください」
WAIT_FOR: ユーザーの承認
RECORD: 承認された優先対象ファイルを「対象リスト」として会話コンテキストに保持する
(以降の PHASE 3・4 の FOREACH はこのリストを参照する)
IF 対象リストが空: 「テスト対象ファイルが見つかりませんでした」を報告して停止する
GATE: 優先対象をユーザーが承認してから PHASE 3 へ進む PROHIBITED: ユーザー承認なしに PHASE 3 へ進むこと
PHASE 3: Characterize(現状固定)
目的: 正しさではなく「今の動作」を記録・固定し、変更後も同じ挙動になることを保証するセーフティネットを作る。 → これを Characterization Test(特性化テスト) と呼ぶ。
NOTE: このフェーズでは構造を変えない。既存コードを読んで動作を記録するだけ。
FOREACH target IN 対象リスト(PHASE 2 で確定、または引数で指定されたファイル):
STEP 1: 現在の動作を把握する
- 代表的な入力でコードを読み、出力・副作用を書き出す
- エッジケース(null・空・境界値・エラー系)での動作も確認する
- 「正しいか」は問わない。「今こう動いている」を記録する
IMPORTANT: コードを読んだ後、このターゲットはテストが書けるか判定する。
┌─────────────────────────────────────────────────────────┐
│ 判定A: テストが書ける → STEP 2 へ │
│ 判定B: 結合度が高いがモジュールはインポートできる │
│ → 統合テスト・E2E で代替する(STEP 2 へ) │
│ 判定C: モジュールのインポート自体が失敗する │
│ (フレームワーク初期化が実行環境を要求する等) │
│ → このターゲットをFOREACHから除外し、PHASE 4「パターン4」を適用する │
│ PHASE 4 完了後、このターゲットのみ STEP 1(この判定)から再実行する │
└─────────────────────────────────────────────────────────┘
STEP 2: Characterization Test を書く
IMPORTANT: 必ず STEP 1 でコードを読んで実際の戻り値・例外・副作用を確認してから
期待値を書くこと。予想で書くと Red になる。
- 入力と実際の出力をそのまま記録する
- テスト名は「[入力条件]のとき[実際の出力]になる」の形式にする
STEP 3: テストを実行し Green を確認する
NOTE: Characterization Test は最初から Green になる
(コードの現状を記録しているため、実行結果と一致するはず)
IF Red になった場合: STEP 1 に戻り実際の挙動を確認し直す
(予想と実際の動作がずれている → 期待値を修正する)
3回以上 Red が続く場合はユーザーに状況を報告して判断を仰ぐ
STEP 4: git commit する
→ この commit が PHASE 4 のリファクタリング中のセーフティネットになる
GATE: 対象リストの全ターゲットについて Characterization Test が全件 Green の状態を確認してから PHASE 4 へ進む PROHIBITED: Characterization Test が Green でない状態で PHASE 4 へ進むこと
PHASE 4: Seam(テスト可能化)
目的: ユニットテストが書けるように構造を変える。ロジックは変えない。
Seam(縫い目) = 振る舞いを変えずに依存を差し替えられる箇所。 ここにモックやスタブを差し込むことでユニットテストが可能になる。
FOREACH target IN 対象リスト:
ANALYZE: 以下のパターンがないか確認する
1. フレームワーク依存の中にビジネスロジックが埋まっている
例: ルートハンドラにバリデーション・DB操作・レスポンス生成が混在
2. 外部依存(DB・API・ファイル・時刻・乱数)を直接呼んでいる
3. グローバル状態・シングルトンへの直接アクセスがある
4. モジュール初期化時の副作用でテスト環境でのインポートが失敗する
例: NextAuth()/DIコンテナ/Spring ApplicationContext がモジュールロード時に
実行環境固有の依存(next/server・サーブレットコンテナ等)をロードする
※ このパターンは PHASE 3 の判定C で先に検出される
IF パターンが1つも発見されない:
NOTE: このターゲットは Seam 不要 → VERIFY へスキップ
ELSE:
FOR EACH 発見したパターン:
PROPOSE: 切り出しの方法をユーザーに提示する(変更内容・影響範囲)
WAIT_FOR: ユーザーの承認
REFACTOR:
IF パターン 1・2・3:
- ビジネスロジックを純粋関数として切り出す(フレームワーク依存なし)
- 外部依存を引数として受け取る形に変える(依存の注入)
- 元のコードはその純粋関数を呼ぶ薄いグルーコードにする
IF パターン 4:
- 純粋ロジックをフレームワーク依存ファイルとは別ファイルへ切り出す
(テスト側は別ファイルをインポートすることでフレームワーク初期化を回避する)
例: lib/auth.ts(NextAuth設定)← lib/auth-helpers.ts(純粋ロジック)
テストは lib/auth-helpers.ts を直接インポートする
VERIFY:
- ビルドが通ること
- PHASE 3 の Characterization Test が全件 Green のままであること
IF Green が崩れた場合: リファクタリングを修正する
修正できない場合: git reset で PHASE 3 の commit まで戻し、ユーザーに報告する
ADD TESTS: 切り出した純粋関数にユニットテストを追加する(正常系・異常系・境界値)
REPORT: 追加したテスト数・ファイル数をユーザーに報告して完了を明示する。 GATE: 対象リストの全ファイルについてユニットテストが全件 Green であることを確認してから PHASE 5 へ進む PROHIBITED: テストが Red のままで PHASE 5 に進むこと 理由: 既存の失敗と新規テストの Red が区別できなくなる
PHASE 5: TDD Forward(以降をテストファーストに)
目的: PHASE 3〜4 で固めた土台の上で、今後の変更をテストファーストで進める。
新機能を追加するとき:
→ 実装前にテストを書く(TDD)
→ Red → Green → Refactor のサイクルで進める
バグが見つかったとき(Bug-fix TDD):
→ バグを再現するテストを先に書く
→ テストが Red になることを確認する
→ バグを修正してテストを Green にする
→ Characterization Test が Green のままであることを確認する
⚠️ PROHIBITED: テストを書かずに直接修正すること
既存コードをリファクタするとき:
→ Characterization Test が Green のままであることを確認しながら進める
→ リファクタ完了後に新しいユニットテストを追加する
参考: テストが書きにくい領域と代替手段
| 領域 | 困難な理由 | 代替手段 |
|---|---|---|
| フレームワークのライフサイクル(DI コンテナ・ミドルウェア等) | 実行環境の再現が困難 | 純粋ロジックを別ファイルへ切り出してフレームワーク依存を分離(Seam パターン4)/統合テスト・E2E テスト |
| 外部 API・メール送信等の副作用 | 実際に呼ぶとコストが発生 | モック(msw・responses 等) |
| DB マイグレーション | 本番 DB への副作用を伴う | インメモリ DB で統合テスト |
| 時刻・乱数依存 | 再現性がない | 時刻・乱数を引数として注入 |
| UI・ブラウザ固有 API | jsdom では再現不可 | Playwright 等の実ブラウザ E2E |
「テストできない」で終わらせず、代替手段を必ず検討すること。