GitHubを駆使したビジュアルリグレッションテスト自動化
こんにちは。すわくんです。
最近は React ばっかり書いています。ついでに CI/CD パイプラインの構築もたくさんやっています。
今回は、GitHub 以外のサービスが使えない制約のもとでビジュアルリグレッションテスト(以下、VRT)を自動化する機会がありましたので、調査・実装した内容についてざっくり紹介したいと思います。
目次
- 情報を整理する
- VRT ツール・手法を調査する
- 実装する
- 今後の課題
- さいごに
1. 情報を整理する
まずは、現状を把握し、ゴールを想像します。
ビジュアルリグレッションテストとは何か?
リグレッションテストのうち、ビジュアルを対象にとるものを指します。今回は UI コンポーネントの見た目に関して、想定外の変化が発生していないことを確認するテスト について考えます。
💡 NOTE: リグレッションテスト → 回帰テスト (Wikipedia)
プロジェクトの現状を把握する
一口に VRT と言っても、プロジェクトの構成や周囲の状況によって、方法や最適解は異なるでしょう。まずはプロジェクトの現状を整理します。
- Storybook (v8)
UI コンポーネントの実装は、すべて Storybook で目視・動作確認している。 - GitHub Flow
すべてのコードの変更は、main ブランチから派生して開始し、main ブランチにマージすることで完了とする。 - GitHub Actions
CI の構築には GitHub Actions が利用可能。Caches や Artifacts も利用可能。ただし、GitHub 公式のアクション (actions/*) 以外は利用不可能。 - その他、GitHub 以外のサービス (SaaS 等) の利用は不可能。
ゴールを想像する
では、どういった状態が今回のゴールになるのかを考えます。
VRT は「ビジュアルリグレッションを防ぐ」ことを目的とします。リグレッションは、ソースコードの変更時に起きる可能性があります。今回、ブランチ戦略に GitHub Flow を採用しているため、ソースコードの変更はすべて main ブランチから派生し main ブランチにマージされることになります。したがって、少なくとも main ブランチに対する Pull Request に対して、そのマージ前に VRT を実行するべきでしょう。
今回は、 Pull Request が作成または更新された時 に main の状態と Pull Request の状態を比較する VRT が実行され、比較結果が確認できる 状態をゴールとします。
2. VRT ツールや手法を調査する
現状とゴールが整理できたので、続けて、どのように実装するか考えます。
今回は Storybook を利用しているため、Storybook 関連の VRT ツールや手法にどんなものがあるのか調べました。
Chromatic
調べると、まず最初に名前が上がるのがこれだと思います。
Chromatic は、Storybook の開発者によって作られた UI テストツールです。VRT も可能で、GitHub との統合もサポートされています。
しかし Chromatic は SaaS であり、スクリーンショットの実行・保存・比較結果レポートの作成などはすべて Chromatic のサーバ上で行われ、レポートは Chromatic のサーバ上にホスティングされるようです。今回は見送ります。
Storycap
恐らくその次にメジャーなツールとして、 Storycap があります。
Storycap は、ヘッドレスブラウザで Storybook の画面を開き、Story ごとのスクリーンショットを撮り、画像ファイルとして保存する、という一連の処理を行う Storybook アドオンです。スタンドアロンの CLI ツールとしても利用可能です。
Storycap はスクリーンショットの画像ファイルを作成するだけですので、その画像を比較して差分を検出するためには、別のツールを併用する必要があります。
Storycap + reg-suit
そこで第一候補として挙がってくるのが、reg-suit だと思います。この形は、今回のように Chromatic が利用ができない状況で、その代替として採用されるケースもあるようでした。
reg-suit は、プラグインの組み合わせにより VRT のワークフローを簡単に構築できるツールです。スクリーンショットファイルの S3 または Google Cloud Storage へのアップロード、比較対象の決定、比較結果レポートの作成、Pull Request へのテスト結果通知、など多くの処理を一手に実行してくれます。
しかし今回は S3 や Google Cloud Storage の利用ができません。また、GitHub Actions の Artifacts に対応したプラグインも現状用意されていません。独自プラグインを実装する形も考えましたが、プラグインのメンテナンスコストが発生することを考えると、できれば避けたいと思いました。
Storycap + reg-cli (採用)
reg-suit の代わりに、その内部で利用されている reg-cli を利用する形を考えました。
reg-cli は、2つの画像セットを比較し、差分がハイライトされた画像と比較結果のレポートを生成する、というシンプルなツールです。オプションを指定すれば、ブラウザで閲覧可能な HTML 形式のレポートも生成できます。
ここまで調べた情報を組み合わせて、GitHub Actions を利用して以下のようなワークフローが構築できそうだと考えました。
- あらかじめ main ブランチの状態で Storycap によるスクリーンショットを作成し、Artifacts にアップロードしておく。
- Pull Request が作成または更新されたとき、その状態で Storycap によるスクリーンショットを作成する。
- 2つのスクリーンショットを reg-cli で比較する。
- 差分の有無を Pull Request に報告する。
- 比較結果レポートを Artifacts にアップロードする。
シーケンス図にしてみると以下のようになります。
💡 NOTE: HTML レポートを GitHub Pages などのホスティングサービスにデプロイする形の方がより手軽だと思いますが、複数の VRT が並行して実行されうることを考えると、1リポジトリで複数の環境が同時に存在できない GitHub Pages は利用に適さないと思います。
3. 実装する
前節で検討したワークフローを実装していきます。
Storycap の実行確認
Storycap を実行する前に、Storybook をビルドします。
storybook build --output-dir storybook-static
続けて Storycap を実行し、スクリーンショットを作成します。
storycap --outDir screenshots --serverCmd 'http-server --port 8080 storybook-static' http://localhost:8080
Storycap は Puppeteer または Chromium のバイナリを利用します。GitHub Actions(ubuntu-latest 環境)で実行することを考えると、Puppeteer を使う方が簡単だと考えました。Storycap コマンドの実行直前に、Puppeteer を利用して Chrome をインストールするようにします。
puppeteer browsers install chrome
main ブランチのワークフロー作成
Storycap による Storybook のスクリーンショット作成が確認できたので、これを利用して main ブランチでのスクリーンショットを Artifacts にアップロードするワークフローを作成します。
on:
push:
branches:
- main
jobs:
storycap-main:
runs-on: ubuntu-latest
steps:
#
# 中略
#
- name: Build storybook
run: storybook build --output-dir storybook-static
- name: Install chrome
run: puppeteer browsers install chrome
- name: Take screenshots
run: storycap --outDir screenshots --serverCmd 'http-server --port 8080 storybook-static' http://localhost:8080
- name: Upload screenshots
uses: actions/upload-artifact@v4
with:
path: screenshots
name: storycap-main
overwrite: true # 最新のスクリーンショットだけ比較に利用するため
このワークフローの追加を main ブランチにマージし、実際に Artifacts が作成されることを確認します。また、ダウンロードして中身が想定通りであることを確認します。
reg-cli の実行確認
main ブランチのスクリーンショットを Artifacts からダウンロードし、ローカルの Expected ディレクトリに保存します。
# GitHub CLI を利用する場合
gh run download --name storycap-main --dir vrt/expected
ローカルの状態に対して Storycap を実行し、スクリーンショットを作成します。これをローカルの Actual ディレクトリに保存します。
storybook build --output-dir storybook-static
puppeteer browsers install chrome
storycap --outDir vrt/actual --serverCmd 'http-server --port 8080 storybook-static' http://localhost:8080
Expected と Actual を比較し、レポートを作成してみます。
reg-cli vrt/actual vrt/expected vrt/diff --json vrt/reg.json --report vrt/index.html
vrt/index.html を開くと、レポートの結果が閲覧できると思います。
Pull Request のワークフロー作成
reg-cli の実行も確認できたので、Pull Request 毎に VRT を実行するワークフローを作成します。
まずは reg-cli
コマンドを実行するところまで作ってみます。
on:
pull_request: {}
jobs:
visual-regression-test:
runs-on: ubuntu-latest
steps:
#
# 中略
#
- name: Download expected screenshots
uses: actions/download-artifact@v4
with:
path: vrt/expected
name: storycap-main
- name: Build storybook
run: storybook build --output-dir storybook-static
- name: Install chrome
run: puppeteer browsers install chrome
- name: Take actual screenshots
run: storycap --outDir vrt/actual --serverCmd 'http-server --port 8080 storybook-static' http://localhost:8080
- name: Compare screenshots and create a report
run: reg-cli vrt/actual vrt/expected vrt/diff --json vrt/reg.json --report vrt/index.html
スクリーンショットに差分がある場合、reg-cli
コマンドは失敗ステータスを返します。つまり、意図した差分のみがある場合(例えば不具合の修正)でも、ジョブが失敗した扱いになります。そういったケースでは、ジョブが失敗していても Pull Request の内容に問題はないはずです。したがって、reg-cli
コマンドが失敗ステータスを返しても、ジョブは成功扱いにするべきでしょう。その代わりに、ラベルを利用して Pull Request に対して差分の有無を知らせることにします。
reg-cli
コマンドが失敗ステータスでもジョブが継続されるようにし、 reg-cli
コマンドのステータスによって Pull Request に付与するラベルを切り替えるようにします。ラベルの付与には GitHub CLI を利用します。
jobs:
visual-regression-test:
runs-on: ubuntu-latest
steps:
#
# 中略
#
- name: Compare screenshots and create a report
run: reg-cli vrt/actual vrt/expected vrt/diff --json vrt/reg.json --report vrt/index.html
continue-on-error: true # 失敗ステータスを無視する
id: reg-cli
# 差分がある場合、vrt:diffラベルを付与
- name: Add label (diff detected)
if: ${{ steps.reg-cli.outcome == 'failure' }}
run: gh pr edit ${{ github.event.pull_request.number }} --remove-label vrt:pass --add-label vrt:diff
env:
GH_TOKEN: ${{ github.token }}
# 差分がない場合、vrt:passラベルを付与
- name: Add label (no diff)
if: ${{ steps.reg-cli.outcome == 'success' }}
run: gh pr edit ${{ github.event.pull_request.number }} --remove-label vrt:diff --add-label vrt:pass
env:
GH_TOKEN: ${{ github.token }}
これで、差分がある場合は vrt:diff
ラベル、差分がない場合は vrt:pass
ラベルが付与されるようになりました。
最後に、reg-cli が生成する HTML レポートを確認できるようにします。ジョブの最後に、Artifacts にアップロードするステップを追加します。これで、比較結果を手元にダウンロードできるようになります。
jobs:
visual-regression-test:
runs-on: ubuntu-latest
steps:
#
# 中略
#
# 比較結果レポート (vrtディレクトリ) をArtifactsにアップロード
- uses: actions/upload-artifact@v4
with:
path: vrt
name: vrt-report-pr${{ github.event.pull_request.number }}
overwrite: true
ここまでの実装で、今回のゴールとした「Pull Request が作成または更新された時 に main の状態と Pull Request の状態を比較する VRT が実行され、比較結果が確認できる 状態」になりました。
4. 今後の課題
シンプルな VRT の自動化は完成しましたが、以下の課題について追加で検討する必要があるでしょう。
- ユーザの操作によって見た目が変化する場合の考慮
- VRT の実行後に main ブランチが更新された場合の考慮
- 比較結果を「ダウンロード」しないと閲覧できないこと
ユーザの操作によって見た目が変化する場合の考慮
要素にフォーカスしたときに色が変わる、クリックするとポップアップが表示される、など、ユーザの操作によって見た目が変化するコンポーネントがあると思います。今回は考慮しませんでしたが、そういった部分もしっかりテストできているべきでしょう。
以下の記事では、Storybook の play 関数を書くのみで対応できそうだとありますので、簡単に対応できそうです。
Storybook の play function と VRT #storybook - Qiita
VRT の実行後に main ブランチが更新された場合の考慮
今回実装した「main ブランチと Pull Request を比較」する VRT は、Pull Request の更新時には再実行されますが、main ブランチの更新時には再実行されません。このとき、テスト結果が事実と乖離する可能性は否定できません。これが原因で困ることは少ないと想像していますが、実際に困ることが多ければ対策する必要があるでしょう。
比較結果を「ダウンロード」しないと閲覧できないこと
reg-cli が生成する HTML レポートについて、今回は Artifacts にアップロードするのみでした。これを確認するためには、いちいち zip ファイルをダウンロードしなければなりません。この点は、Chromatic や reg-suit を利用した場合の体験と比べて、明らかに劣る部分だと思います。
Chromatic を利用するか、Amplify、Vercel、Netlify といったホスティングサービスにデプロイする形がベターでしょう。または、もし将来 GitHub Pages が複数環境のデプロイに対応したのならば、今回の制約はそのままに解決できるでしょう。
5. さいごに
本記事では、GitHub 以外のサービスが使えない制約のもとで、Storybook、Storycap、reg-cli を組み合わせ、GitHub Actions を駆使した VRT を実装しました。外部サービスを併用する形と比べると体験は劣りますが、VRT として最低限の形にすることは出来ました。
今回実装した CI が実行されて Pull Request にラベルが付与されるのを実際に見ると、自信をもって Pull Request をマージできるようになりました。VRT を導入し自動化することで、目視よりも正確にチェックでき、さらに時短にもなります。積極的に導入していきましょう。
サムネイル左: GitHub ロゴ
サムネイル右: Storybook ロゴ