先に結論だけ
- 1ファイル数千行が当たり前、数年前に作られてdeprecatedな古い関数も多く残っている——そんなレガシーな大規模プロジェクトを「動作を変えずに」リファクタしたい、という現場はよくある。
- このとき素直に「リファクタ前に単体テストを全部書く」を目指すと、たいてい詰まる。単体テストを書くには抽出(=実装変更)が必要で、それを保護なしでやることになるから。
- 解決は 二段階戦略:
- **第1段階(リファクタ前)**=外から観測できる振る舞いを保護(ゴールデンマスター / API契約 / UIの振る舞い)
- **第2段階(リファクタ中〜後)**=抽出してテスト可能になった単位から単体テストを足す
- 「リファクタ前に単体テスト全部」を必須にしないのが肝。
よくある現象:「テストが書けない」のは設計のサイン
数年運用されてきた巨大ファイルにありがちなのは、ロジックがコンポーネント/クラスの内部に未exportのまま埋め込まれている状態です。しかも、もう呼ばれていないdeprecatedな関数や、どこから使われているか追い切れない処理が混ざっている。この内部ロジックを単体テストしようとすると、まず関数として切り出す必要があります。つまり——
単体テストを書くには、先にリファクタ(抽出)しなければならない。
でも安全にリファクタするには、先にテストが要る。
という鶏卵が発生します。おまけに「どの関数が今も生きているか」さえ怪しいので、内部に手を入れる前提のテストは特に危うい。この構図は次の2記事がそのものズバリです。
ここで効くのがAutify記事の 「低いテストレベルの実装が困難な場合は、高いテストレベルから始めて、順番に低いレベルも実装していく」 という考え方です。
二段階戦略
| 段階 | タイミング | 目的 | 主な手段 | 必須? |
|---|---|---|---|---|
| 第1段階 | リファクタ前 | 外部仕様の保護 | golden-master / API契約 / UI振る舞い | 必須 |
| 第2段階 | リファクタ中〜後 | 内部実装の単体化 | 抽出 → 純粋関数の単体テスト | 単位ができた都度 |
要は 「リファクタ中に動かない境界」にテストを置くだけです。
- サーバ内部を作り替える → 動かない境界は API(request→response)
- 画面側を作り替える → 動かない境界は ユーザー操作(ロール/ラベル/テキスト)
内部構造に触れないので、中をどう作り替えても——deprecatedな関数を消しても——通り続けます。
実装詳細に依存させないコツ(リファクタ耐性)
第1段階のテストが内部実装に密結合すると、それ自体がリファクタの足かせになります。避けるための一般的な原則:
- DOMのクラス名や要素ツリーで取得しない → 画面はロール/ラベル/表示テキストで取得する。
- 「AがBを呼んだ」式のモック検証をしない → 入力→出力という契約だけを見る。
- HTML丸ごとのスナップショットを固定しない → 値・表示テキスト単位で確認する。
- 計算系・整形系は golden-master(現状の入出力を記録し、変化しないことだけ検証)。
E2Eをどう扱うか
「ログイン→作成→計算→保存」のような通し確認は価値が高い一方、自動E2Eは壊れやすく保守コストが高い。なので一般的な落としどころは:
重要フローだけを薄く(自動E2E or 手動チェックリスト)/網羅は単体・API契約で広く取る。
ピラミッドの頂点を厚くしないのがポイントです。
学び
- 「単体テストが書けない」こと自体が設計の問題のサイン。無理に書くと密結合し、リファクタの妨げになる。
- 第1段階は外側を黒箱で固め、第2段階で「リファクタによってテスト可能になった単位」から単体化する。“テスト可能になった”こと自体が設計改善の成果指標になる。
- deprecatedな関数の整理も、外部仕様が固定されていれば「消して通ればいらなかった」と安全に判断できる。