2007年に執筆した『Smarty動的Webサイト構築入門』のサンプルコードリポジトリを現在もメンテナンスしています。
このリポジトリの目的は「レガシーSmartyの担当者になってしまったエンジニア」に向けて、実際のコードやリファクタPRを例にしながらレガシーPHPのリファクタリング実践例を示すことです。
今回は、数ある取り組みの中でも特にバージョンアップやリファクタリングの安心感を支えてくれた「Golden Master Test(スナップショットテスト)」について、導入のメリットや運用の工夫をご紹介します。
1. レガシーPHPのバージョンアップ、怖くありませんか?
テストコードがほとんど存在しない、あるいは古い仕様のまま何年も放置されたレガシープロダクト。こうしたコードに対して、PHPのバージョンアップ、PHPStanの導入、あるいはライブラリのアップデートを行うのは、地雷原を歩くような緊張感を伴います。
「ここを変更したら、別のページの表示が崩れるのではないか」 「気づかないうちにエラーが発生しているのではないか」
そんな不安を解消するために導入したのが、既存のHTML出力をそのまま「正解(ゴールデンマスター)」として保存し、変更後の出力と比較し続けるGolden Master Testです。
この仕組みを導入したおかげで、PHP 7.4から8.0、そして8.5へのアップデートを安全にやり切ることができました。さらに、CI環境での自動化を進めた結果、RenovateやDependabotのメジャーバージョンアップデートPRの自動マージまで実現できています。
2. Golden Master Test とは?
Golden Master Test(スナップショットテストの一種)は、個々の関数やメソッド単位で細かいアサーションを書くのではなく、アプリケーションの最終的なHTML出力結果を丸ごとファイルに保存し、変更前後の差分を検証するテスト手法です。
カバレッジ100%のユニットテストを最初から書こうとすると膨大な労力がかかりますが、Golden Master Testであれば、主要なURLやPOSTリクエストの網羅的な出力を一網打尽にスナップショットとして固定できます。
ここで一つ重要なポイントがあります。Golden Master Test は、個々のロジックの正しさを保証する「単体テスト(Unit Test)」の代わりにはならないということです。
あくまで「既存の出力結果から意図しない変更(退行)が起きていないか」を検知するための回帰テスト(リグレッションテスト)であり、ビジネスロジックの正確な仕様を担保するものではありません。しかし、テストのないレガシーコードにおいて、「全体が大きく壊れていない」という安心感を低コストで得るための「最初の一歩」としては、これ以上ない強力な武器になります。
3. Golden Master Test を導入してよかったこと
実際に導入して運用する中で、大きなメリットを実感しています。
大規模リファクタ・バージョンアップの恐怖がなくなる
カバレッジが不完全な状態であっても、「正常系操作の大部分のHTML出力が以前と変わっていない」という保証が自動で得られるため、コードを書き換える心理的ハードルが劇的に下がりました。
依存関係アップデートの自動化(Auto-merge)
テストをCI(GitHub Actions)上で回すようにしたことで、RenovateやDependabotが作成する依存関係のメジャーバージョンアップPRを、人間が毎回手動で確認しなくても安心して自動マージできるようになりました。
シンプルな構成で広範囲をカバー
URLやPOSTデータをCSVで定義し、そこからテストデータを動的に生成する構造にしているため、テストケースの追加や管理も非常にシンプルです。
4. 実際に運用で工夫したポイント
動的なWebアプリケーションのHTML出力をそのまま比較する場合、そのままでは「毎回変わる値(日時やトークンなど)」によってテストが落ちてしまいます。これを実用に耐えるものにするため、いくつかの工夫を取り入れました。
① ノイズ(動的な値)を弾くサニタイズ処理
GoldenMasterTest::sanitizeHtml メソッドを実装し、比較前に以下の項目をマスク・置換するようにしています。
- 日時
- CSRFトークン
- JS・CSSのキャッシュバスター(例:
fuga.js?v=0123abcd→fuga.js?v=VERSION) - サンプルコードで表示している Smartyバージョン
- その他、環境依存のパスやセレクトボックスの動的な選択状態など
実際にどのようなHTML文字列のサニタイズを行っているかは、テストコード側で細かく担保しています。

private function sanitizeHtml(string $html): string
{
$html = preg_replace(
'/(<input type="hidden" name="csrf_token" value=")[0-9a-f]+(")/',
'${1}CSRF_TOKEN${2}',
$html,
);
// script の src に含まれるバージョン値を置換
$html = preg_replace(
'/(<script\b[^>]*\ssrc=["\'][^"\']*\.js\?v=)[^"\'&\s>]+/i',
'${1}VERSION',
$html,
);
② セッション・Cookieを引き継いだクロール
CrawlCode::crawl では、cURLの COOKIEFILE / COOKIEJAR を活用してCookieを継続して引き継げるようにしました。これにより、セッションデータを使ったページや、一連の画面遷移を伴うテストもスムーズに行えるようになっています(※現時点ではCSRFトークンの厳密な検証までは対応していませんが、正常系の動作確認としては十分機能しています)。

テスト対象のURLやメソッド、POSTデータは次のようなCSV(url.csv)で管理しています。

ページ名,メソッド,URL,POSTデータ # chapter2 chapter2/02_01,GET,http://localhost/smartybook/chapter2/02_01.php, chapter2/02_03,GET,http://localhost/smartybook/chapter2/02_03.php?category=Study, chapter2/02_05,POST,http://localhost/smartybook/chapter2/02_05.php,category=Study&title=test-title&contents=test-body&date=1789462161&image=
③ CI(GitHub Actions + Docker)の高速化・最適化
GitHub ActionsのVM環境上でDockerコンテナを起動してテストを実行しています。 コンテナビルド時には docker/setup-buildx-action を利用してキャッシュを有効活用できるようにし、さらに docker compose up の際に不要なサービス(pmaなど)が起動しないよう、php-web と mariadb-db を明示的に指定して無駄なダウンロードや時間を削っています。

- name: Build with Buildx Bake
uses: docker/bake-action@v7
with:
files: docker-compose.yml
targets: php-web
load: true
set: |
php-web.tags=smartybook-php-web:latest
*.cache-from=type=gha
*.cache-to=type=gha,mode=max
手元やCIからMakefile経由で簡単に実行できるようにタスク化しています。
.PHONY: php-test-golden-master php-test-golden-master: docker compose up -d docker compose exec php-web composer run-script test:golden-master .PHONY: php-test-golden-master-update php-test-golden-master-update: docker compose up -d docker compose exec php-web composer run-script test:golden-master-update
④ あえて「やらなかったこと」の割り切り
HTMLの空白やインデントの差異を正規表現で完璧に吸収しようとすると、ルールが複雑化してCopilotのレビューも収束しなくなります。そのため、「厳密な空白の無視は追い求めすぎず、必要に応じて目視チェックも許容する」という割り切りを行い、メンテナンス性を維持しました。
レガシーアプリケーションをブラックボックスとして扱い、その出力結果(HTMLやスクリーンショット)を検証するアプローチには、いくつかの選択肢が考えられます。
今回採用した自作スクリプトを含め、代表的な4つの方法をそれぞれの特徴とともに比較します。
5. レガシーアプリの検証手法:4つのアプローチの比較
レガシーな .php ファイルやテンプレートが散らばるアプリケーションに対してゴールデンマスターテストやスナップショットテストを導入する場合、次のような手法が挙げられます。
1. CSV管理 + cURL + PHPUnit(またはPest)による自作スクリプト 【今回採用】
- 概要:URLやPOSTデータをCSVで一覧化し、cURLでリクエストを投げて得られたHTMLをサニタイズした上でPHPUnit等で比較する手法。
- 特徴:アプリケーション側のソースコードに一切手を加えず、純粋なブラックボックスとして外側から全エンドポイントを検証できます。他プロジェクトでの実績があったので採用しました。
2. Spatie Snapshot などのライブラリを活用する手法
- 概要:PHPの著名なスナップショットテスト用ライブラリ(
spatie/phpunit-snapshot-assertionsなど)を導入し、テストコード内で出力結果を比較する手法。 - 特徴:アサーションの比較部分は非常にスマートになりますが、「どのURLをどうやって集めて回るか(クロール部分)」の仕組みは自前で用意する必要があります。今回のレガシー構成ではそこまで大きなメリットを見出せませんでした。
3. フレームワークのHTTPクライアント + アサーション
- 概要:LaravelやSymfonyなどのフレームワークが持つテストクライアントを使ってルーティングを直接叩く手法。
- 特徴:モダンなフレームワークであれば強力ですが、独立したスクリプトや古いインクルードファイルが点在するレガシーアプリでは、全エンドポイントにテスト用の仕込みやルーティングの統一が必要になり、改造コストが高すぎて現実的ではありません。
4. Playwrightのスナップショット機能(VRT)
- 概要:ヘッドレスブラウザを使い、画面の描画結果やHTMLスナップショット、さらには視覚的な差分(VRT)まで丸ごと比較する現代のデファクトスタンダード。
- 特徴:もし「既存のレガシーコードには一切手を加えずに、真っさらな状態から検証環境を作る」のであれば、これが第一候補です。コードを汚さずにブラウザレベルのレンダリング結果まで含めてブラックボックス検証できるのは非常に強力です。
どの手法を選ぶべきか?
今回の『Smarty動的Webサイト構築入門』のサンプルコードのように、レガシーな個別スクリプトの集まりであり、すでに他プロジェクトで安定稼働している実績ある知見があったという文脈では、今回組んだ「CSV + cURL + 自作サニタイズ」というアプローチがもっとも現実的で手堅い選択でした。
一方で、もしこれから完全にゼロベースでレガシーアプリのテスト網を再構築する機会があれば、ブラウザの描画やレイアウト崩れまで一網打尽に検知できる PlaywrightによるVRT を選択するのが最もモダンで確実なアプローチと言えそうです。
6. まとめ
レガシーなPHPアプリケーションやSmartyのコードを現代の環境に合わせていくとき、最初から完璧な単体テストを書こうとすると、テストを書くこと自体が負担になって手が止まってしまいます。
前述の通り、Golden Master Test は単体テストの代わりにはなりません。 個別のロジックや仕様の正しさを証明するものではなく、あくまで「全体が予期せず壊れていないか」を網羅的に検知するためのセーフティネットです。
一方で、Golden Master Test の準備は思っているよりも簡単で、得られる安心感に対して圧倒的にコスパが高い施策です。
実際に運用を始めると、AI(Copilotなど)もこのテストの存在をきちんと認識してくれて、コードを修正した際にプルリクエストのレビューで「Golden Master のほうのフィクスチャも更新してください」と指摘してくれるようになります。そんなやり取りを見るたびに、「ああ、Golden Master Test がこのプロジェクトの一部としてしっかり根付いてくれたんだな」と、胸をなで下ろすような安心感を覚えます。
テストのないレガシーコードを修正することになったら、まず修正箇所の Golden Master を作る。別の箇所を修正することになったら、その箇所の Golden Master を作る。少しずつ Golden Master を増やしていくのも、レガシーコードを健全化する立派な作戦の一つです。
安全にコードを洗練させていくための第一歩として、ぜひ取り入れてみてはいかがでしょうか。

