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