2024年06月20日

GitHubを駆使したビジュアルリグレッションテスト自動化

こんにちは。すわくんです。

最近は React ばっかり書いています。ついでに CI/CD パイプラインの構築もたくさんやっています。

今回は、GitHub 以外のサービスが使えない制約のもとでビジュアルリグレッションテスト(以下、VRT)を自動化する機会がありましたので、調査・実装した内容についてざっくり紹介したいと思います。

目次
  1. 情報を整理する
  2. VRT ツール・手法を調査する
  3. 実装する
  4. 今後の課題
  5. さいごに

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 が実行され、比較結果が確認できる 状態をゴールとします。

mainブランチの最後のコミットとdevelopブランチの最後のコミットとを比較する図

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 を利用して以下のようなワークフローが構築できそうだと考えました。

  1. あらかじめ main ブランチの状態で Storycap によるスクリーンショットを作成し、Artifacts にアップロードしておく。
  2. Pull Request が作成または更新されたとき、その状態で Storycap によるスクリーンショットを作成する。
  3. 2つのスクリーンショットを reg-cli で比較する。
  4. 差分の有無を Pull Request に報告する。
  5. 比較結果レポートを Artifacts にアップロードする。

シーケンス図にしてみると以下のようになります。

VRTワークフローのシーケンス図

💡 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 ロゴ

RELATED POSTS