既存コード修正の前に現行仕様のテストを書くべき理由
はじめに
既存コードの修正は、実装を先に触りたくなりがちです。
ただ、先に現行仕様をテストで固定しておくと、結果として修正の安全性と速度が上がることが多いです。
この記事では、なぜこの順番が有効なのか、生成AIを使った実装やリファクタリングの文脈も含めて整理します。
特に最近は生成AIで修正案を作る機会が増えたので、この順番の重要性を以前より強く感じるようになりました。
先に現行仕様のテストを書く理由
- 今の挙動をチームで共有できる形で残せる
- 変更時の退行を自動で検知できる
- 仕様なのかバグなのかを切り分けやすくなる
- 修正範囲の見積もりが現実的になる
- 仕様変更の意図をテスト差分から追いやすくなる
- ビジネスサイドとエンジニアの現行仕様認識のズレを可視化しやすくなる
コードコメントや口頭説明より、テストのほうが「何を守るべきか」が明確です。
実装だけを読むより、テストの追加や更新を見るほうが「今回どこを仕様として変えたのか」を把握しやすい場面は多いです。
実際には、仕様案を出したビジネスサイドと実装側の認識がずれていることもあります。
現行仕様をテストとして明文化しておくと、どこが合意できていて、どこが未合意かを会話しやすくなります。
このときに作るのは、いわゆる characterization test(現行挙動の記録)に近いテストです。
例外ケース: 不具合を固定しないための線引き
現行仕様を固定するアプローチには、既存の不具合まで固定してしまうリスクがあります。
そのため、明らかな不具合だと分かっている挙動は「現行挙動を守るテスト」ではなく「期待仕様を表すテスト」として先に定義するほうが安全です。
判断に迷う場合は、次のように扱うと整理しやすくなります。
- 現行仕様として合意されている挙動は、現行挙動テストとして固定する
- 不具合と合意できる挙動は、修正後の期待値をテストにする
- 合意がない挙動は、テスト追加前に仕様確認する
作者追記の補足: 修正前リファクタリングとの相性が良い
この考えに至った大きな理由は、コード修正の前にリファクタリングしたほうが良いケースが多いからです。
既存コードが読みにくい状態のまま、生成AIに実装やテストを作らせると、次の問題が起きやすくなります。
- 意味の薄い変数名や関数名をそのまま引き継いでしまう
- 責務が混ざった関数に対して、同じ構造のままテストを量産してしまう
- 「通るけど読めない」テストが増えて、保守性が下がる
先に現行仕様テストで振る舞いを固定しておけば、安心して命名整理や関数分割に進めます。
そのうえで実装変更を行うと、テストの意図も実装の意図も揃いやすくなります。
また、自然にテストを書こうとすると、責務ごとに関数を分ける必要が出てくるため、関数分割が進みやすくなります。
その過程で関数名も見直されるので、既存コードが何を意図しているのかを読み取りやすくなります。
テストがあると危険なリファクタリングにも踏み込める
テストがない状態だと、既存コードの構造を大きく変える判断はかなり怖いです。
一方でテストが先にあると、失敗をすぐ検知できるため、構造改善の選択肢が増えます。
- 大きな関数の分割
- 責務ごとのクラスやモジュールへの再配置
- 命名の全面的な見直し
- 重複ロジックの統合
こうした変更は、仕様変更が目的ではなく保守性改善が目的です。
先にテストで現行仕様を固定しておくと、「動きは変えずに構造だけを変える」作業を進めやすくなります。
IDEのリファクタリング機能との相性も良いです。
IDEで一括リネームやシグネチャ変更を行い、テストで結果を確認する流れにすると、変更の安全性が上がります。
さらに、テストが十分にあると、IDEの自動変換だけでは扱いにくい設計レベルの見直しにも着手しやすくなります。
たとえば、責務の分離方針そのものを変えるようなリファクタリングでも、テストがあれば段階的に安全確認しながら進められます。
よくある失敗
修正を先に進めてからテストを書くと、修正後の挙動を正解として固定してしまいがちです。
この状態だと、本来守るべき既存仕様が消えていても気付きにくくなります。
また、AIが提案したコードをそのまま採用した場合、修正の目的と関係ない部分まで変化していることがあります。
テストの足場がないと、どこまでが意図した差分なのか追跡が難しくなります。
実践するときの流れ
- 現在の振る舞いを確認する
- 挙動をそのままテストに落とし込む
- テストが通ることを確認する
- 必要なら先に小さくリファクタリングする
- 本来の仕様変更や不具合修正を行う
- 変更した仕様に合わせてテストを更新する
先に「今の正しさ」を定義すると、リファクタリングと仕様変更を分離できるのでレビューもしやすくなります。
一度、現行仕様テストを書いてからリファクタリングしてみると、この時点で認識違いに気づけることがあります。
その気づきを材料にすると、そもそも修正が必要か、修正内容は本当に正しいかをビジネスサイドと議論しやすくなります。
実装を大きく変える前に合意を作れるため、あとからの手戻りを減らせます。
どこからテスト化するか
「全部テストしてから直す」は理想ですが、現実的ではないことも多いです。
その場合は、次の順番でテスト化すると効果が出やすいです。
- 影響範囲が広い共通処理
- 過去に障害や手戻りが出た箇所
- 仕様が複雑で認識ズレが起きやすい箇所
- 今回の修正対象の直近周辺
まずは事故が起きやすい場所から固定し、修正に入るのが実務では効率的です。
シナリオテストより単体テストを優先する理由
既存コードを安全に直すなら、シナリオテストを厚くするより、先に単体テストを書くほうが有効です。
一番の理由は、変更の影響範囲を小さく保てるからです。
シナリオテストは全体の流れを確認するには有効ですが、失敗したときに原因が広くなりやすいです。
入力整形、条件分岐、計算、永続化など複数の責務が1つの失敗に重なるため、調査コストが上がります。
単体テストを先に置くと、どの関数・どの責務で挙動が変わったかを狭く特定できます。
その結果、修正する実装の範囲も、更新するテストの範囲も小さくなります。
つまり、既存挙動を確認するときは、変更が起きる箇所に近い単位でテストを書くほうが、
「どこを変えたのか」「何が変わっていないのか」を明確にしたまま進めやすい、というのがこの方針の中心です。
小さな具体例
たとえば、金額計算ロジックの関数を整理したいケースを考えます。
先に現行仕様テストを作っておけば、次のように安全に進められます。
- 現行の計算結果をパターンごとにテスト化する
- 長い関数を分割し、命名を整理する
- テストが通ることを確認する
- その後で新しい割引仕様を追加し、テストを更新する
この順番だと「構造改善」と「仕様変更」を別々に検証できるので、レビュー側も差分を追いやすくなります。
レビューで見やすい差分を作るコツ
実装変更とテスト変更が同時に入ると、意図が読み取りづらくなります。
次の観点を意識すると、レビューの負担を減らせます。
- リファクタリングのコミットと仕様変更のコミットを分ける
- 仕様を変えていない段階では、テスト名も期待値も原則変えない
- 仕様変更時は、テストの追加理由をPR本文に一言添える
「何を守った変更か」「何を変えた変更か」を分離するだけで、合意形成の速度がかなり変わります。
まとめ
既存コード修正の前に現行仕様テストを書くのは、短期的には遠回りに見えます。
それでも、手戻りや調査コストを減らせるため、最終的には開発速度と品質の両方を上げやすいです。
仕様変更の説明コストも下がるので、レビューや引き継ぎにも効きます。
修正規模が大きいほど、先にテストで足場を作る価値は高くなります。
特に、変更規模が大きい案件や影響範囲が広い機能修正では、この順番の効果が出やすいです。
よりリファクタリング寄りの観点で整理した内容は、次の記事にもまとめています。
関連: 基本的にコード修正の前にリファクタリングするべき
Discussion