技術公開日 更新日

創業者手作り10年ものPHP5システムをLaravelへ刷新しよう(3) -テストコードで挙動を保証する-

はじめに

前回の記事では、Laravel移植とインフラ移行の実録を記録しました。

今回はその3として、テストコードによる挙動の保証について記録します。

①負債の把握 →
②Laravel/インフラ移行 →
③テストコードで保証(今ここ!) →
④リファクタリング&業務改善


もうテストコードは必須としましょう

経営者・ビジネス側の方へ。

テストコードとはプログラムが書いた意図通りに動くかをさらにプログラムを書いて確かめるコードのことです。つまり、プログラムのためのプログラムです。そうすると、「テストコードがなくても動くのでは?」とか「追加のコストでは?」と感じるかもしれません。しかしテストコードはプロダクトコード(プログラム本体)とセットであって、書くことが当たり前のものです。車にシートベルトがついているように、システムにはテストコードがあるものと思ってください。

テストコードがあると、「新機能をリリースしたら、既存機能が動かなくなった」とか「こっちを直したらあっちがバグった」という事態を高い確率で防ぐことができます(※完全に防げると言ってない)。

今回のプロジェクトでは、素のPHP5系ではそもそもテストコードを書ける環境が存在しませんでしたので、この話はLaravelへの移植後になります。テストコードの導入により何が起こるのかをご覧ください。


テストの目的 ー明日のために今の動作保証をするー

テストコードを導入する目的は、今後発生する技術負債の返済_ーぐちゃぐちゃなコードを整理したり、不要なロジックを削除したり、ユーザーもスタッフも誰も使っていない謎の機能を削除したり、新機能を追加しやすいよう拡張性を確保したりー_ の際に意図しないコードの修正を防ぐために、コード修正前後で必要な機能が同様に動いていることを保証することです。

まずは結合テストで挙動を保証する

さて、Laravel移植直後の段階では、コード自体がまだテストしやすい構造になっていませんでした。前回記録したResponse層のような設計であり、きれいな単体テストを書ける状態ではありません。

そこでこの段階では、ControllerのメソッドレベルでRequest/Responseとデータを確認する結合テストに近い形でテストを書きました。完璧な単体テストより、まず「この画面はこのリクエストに対してこのレスポンスを返す」という挙動を保証することを優先しました。

どの機能を優先したか

画面表示だけで完結するような箇所は省略し、ビジネスの核心となる業務フローを中心にテストを書きました。具体的には以下のとおりです。

生徒側の業務フロー

  • 生徒の登録
  • 決済
  • 予約成立
  • 授業評価
    講師側の業務フロー
  • 講師の登録
  • スケジュール登録
  • 予約成立
  • 授業評価
  • 報酬計算
    これらの正常系を中心にテストを書きました。このサービスの収益と信頼を支える核心部分であり、ここが壊れるとビジネスへの影響が最も大きいからです。

テストで「防げなかった」こと、「支えられたこと」

切り替え後に発覚した例外ケース

テストコードはあくまで現行動作を担保するものであって、テストコードによってロジックの矛盾や仕様の誤りを発見するということは難しいです。

”創業者だけが知っている謎ルール” の動作をテストすることはできますが、この謎ルールがなぜ存在するか・どういう経緯で作られたか、を追って確認するにはまだまだプロダクトへのコミットが必要でした。
また、そうしたロジックはビジネス側の手動テストでもカバーしきれないパターンとなります。

実際にLaravelへ切り替えた後、古参ユーザーの方からの指摘や普段使わないような操作によって特殊なパターン・例外的なケースが発覚し、その都度確認・修正が発生しました。

これはLaravelへの移行漏れで、Laravel移行後のテストコードではやはりカバーしきれませんでした。
テストコードは「切り替え前後の移行漏れを防ぐ」ためのものではなく、あくまで「切り替え後の動作保証という位置づけでしたので、こうしたLaravelへの移行によって動作が変わってしまったもの(ビジネス側のテストもすり抜けてしまったもの)についてははその都度対応しました。

「完璧なシステム」より「すぐ直せるシステム」

さて、テストコードもそうですが1人エンジニア体制というリソース制約の中でこのプロジェクトは進んでいます。この体制で目指すべきは結合テスト・統合テストまでを完璧にカバーしてエラーゼロを目指すことではありません。

システムは完璧を目指すほど(完璧に近づくほど)コストが掛かってしまいますので、このあたりは割り切りが必要です。

そこで
「完璧なシステムに固執するより、エラーが出てもすぐに直せるシステムを目指す。」
という方針としました。

この判断のもと、テストコードの最終的な役割は以下の3つと定めました。

  • Laravel移植後の動作保証
  • PHPやLaravelのバージョンアップ時の安全性を高めるための安全網
  • 実装ミスを早期に発見するための補助ツール(テストを書くことで自分の実装の意図と差異に気づきやすくなります。今ならAIの自動レビューが担う部分でもあります)
    カバレッジを指標としながら、この3つを満たすテストコードを書き続けることにしました。

リファクタリング後:単体テストへの移行

リファクタリングは単体テストを実施しやすいテスタブルなコードにすることも目標の1つです。
ロジックが整理されるにつれて、テストコードも結合テスト中心から単体テストレベルへと移行していきました。

最終的には単体テストのみの構成としました。リファクタリングによってロジックが1箇所に集約され、単体テストで十分にカバーできる構造になったからです。

この単体テストが、その後の年1回のPHP/Laravelバージョンアップで実際に機能しました。バージョンアップ時にテストを回すことで影響範囲をすばやく確認でき、1人エンジニア体制でも安心してバージョンアップを進められる環境を維持できました。


ビジネス側から見たテストコードの価値

ビジネス側がテストコードの恩恵を感じられるかといえば、それは率直に言って難しいと思います。エラーが減った・バージョンアップがスムーズだったという事実は、「何も起きなかった」という形でしか現れないからです。

ただエンジニアの視点から言えば、1人体制でシステムを安全に維持し続けるための土台として、テストコードの存在はとても心強く今振り返ると不可欠でした。見えないところで事業継続性を支えるものとして、テストコードへの投資は十分に価値があったと確信しています。


まとめ

この記事では、Laravel移植後のテストコード戦略を振り返りました。

ポイントを整理すると以下のとおりです。

  • テストコードはプロダクトコードとセットであり、追加コストではなく事業継続性への投資です
  • 移植直後は結合テスト中心で挙動を保証し、リファクタリング後に単体テストへ移行するという2段階の戦略が有効でした
  • **「完璧なシステム」より「すぐ直せるシステム」**を目指す判断が、1人エンジニア体制では現実解です
  • テストコードの最大の価値はバージョンアップ時の安全網として長期的に発揮されます

次回はリファクタリング・業務フロー改善、そしてプロジェクト全体を完走して思うことを記録します。


← 記事一覧に戻る