クラウドワークス エンジニアブログ

日本最大級のクラウドソーシング「クラウドワークス」の開発の裏側をお届けするエンジニアブログ

フロントエンド開発体験向上のために VRT を導入してみた

アイキャッチ:フロントエンド開発体験向上のために VRT を導入してみた

はじめに

こんにちは、ジャンヌチームです。 前回のVue3 移行記事に引き続き、フロントエンド周辺の改善をおこなっています。 今回は開発体験の向上を目的とした、 VRT の導入の記録となる記事になります。

VRT について

VRT とは Visual Regression Testing のことで、画像による回帰テスト、つまり画像の変更をテストするものです。

クラウドワークスでは Vue3 移行に伴い、ほぼ全ての Vue.js コンポーネントで Storybook を定義しています。 この Storybook 定義を用いて、Storyshot による DOM レベルの自動回帰テストを行っています。

しかし CSS の変更におけるビジュアルテストは担保できていなかったため、VRT を導入することにしました。

VRT 調査対象

VRT を導入するにあたり、どのツールを用いるとよいか調査をしてみることにしました。

Chromatic

Chromatic は Storybook を使った VRT のマネージドサービスです。Storybook でも VRT 事例として紹介されていたり、導入する企業も見られていますが、従量課金で画面単位で行うため、ツールとして高額になる可能性があり、不採用になりました。

storybook-chrome-screenshot

Storycap の元となったものです。発生していた課題を解決して Storycap が開発された経緯があり、現状は Storycap の方が採用例が見られるため不採用になりました。

Cypress

VRT のマネージドサービスとして取り上げてみました。 クラウドワークスでは過去に e2e テストとして導入していたこともあるのですが、Storybook の資産を生かすには別途実装が必要になるため、今回は不採用としました。

Storycap + reg-suit

github.com

クラウドワークスでは Storyshot という plugin を用い DOM レベルのスナップショットを、正常とされているレポジトリ上のデータと比較を行い、差分があれば CI でエラーとしています。

Storycap は同様に内部で puppeteer を用い、Storybook の実行内容のスクリーンショットを取得するツールです。

併せて使用される reg-suit は、画像の差分を取った結果をレポートするツールです。バックエンドとして標準で AWS S3 などのアドオンをサポートしており、CI に組み込まれて自動テストを行うことがとても簡単にできます。

導入企業・サービス例

上記の調査結果より、Storycap + reg-suit を導入してみることにしました。

VRT で導入してみた各技術の所感

Storycap

使っていた感想ですが、最高!でした。 Storycap はほぼ設定無しでいきなり動作してびっくりしました。最終的にはある程度設定を加えましたが、とっかかりの部分がすぐ動くと勢いがついてとても良いです。

Storybook を立ち上げ、アクセス可能な状態で npx storycap <URL> を行うだけで実行が可能です。

Storybook の addon として Storycap を指定した場合、Storybook の他の設定と同様に parameters から設定を追加することができます。

例えば

などの設定を行うことができます。

参考:Storycapのドキュメントのtype ScreenshotOptions

reg-suit

こちらの感想も同じく、最高!でした。 設定ファイルでは以下のようにしています。

- name: Reg-suit
   run: |
    export VRT_BASE_SHA="${{ github.event.pull_request.base.sha }}"
    export VRT_HEAD_SHA="${{ github.event.pull_request.head.sha }}"
    yarn reg-suit run
  "core": {
    "thresholdRate": 0.0001,
  }
  "plugins": {
    "reg-simple-keygen-plugin": {
      "expectedKey": "${VRT_BASE_SHA}",
      "actualKey": "${VRT_HEAD_SHA}"
    },
    "reg-publish-s3-plugin": {
      "acl": "private"
    }
  }
  • core.thresholdRate: 0.0001
    • 良い値が決めきれなかったので、とりあえずの値になります。
  • plugins.reg-publish-s3-plugin.acl: private
    • VRT のための画像は内部からのみアクセス可能にしています。S3 上で public にはしません。
  • plugins.reg-simple-keygen-plugin
    • 差分の比較元と先は好みだと思いますが、PRの base と head にしました。

実際はもう少し複雑で、複数系統の Storybook が存在する関係で Matrix ビルドを行なって、結果を一つの Issue comment として残しています。

カスタマイズ

reg-suit での Issue comment を自由に定義したかったため、plugin を作成しました。 この plugin は Notifier plugin にわたる NotifyParams をそのまま JSON で出力し artifact として保存します。

GitHub Actions の actions/github-script を利用して Matrix build された複数の artifact/JSON を読み込み、一つのコメントにしています。 ヘッダ行が一致する古いコメントは同時に削除をおこなっています。

設定は以下のようになっています。

  notify-storybook:
    runs-on: ubuntu-latest
    timeout-minutes: 5
    needs: [frontend-storybook]
    steps:  
      - name: Download results
        uses: actions/download-artifact@v2
        with:
          path: results
      - name: Notify
        uses: actions/github-script@v5
        with:
          script: |
            const storybookResult = JSON.parse(require('fs').readFileSync('./results/result-storybook/output.txt', 'utf-8'));
            # 複数の Result があったので同様に列挙しました。
            const branch = '${{ github.event.pull_request.head.ref }}';
            const header = '# Storybook results';
            const createCommentText = (name, params) => {
              const { passedItems, failedItems, newItems, deletedItems } = params.comparisonResult;
              # ...コメント文章を生成して返す
            };
            const commentText =
              header + '\n\n' +
              createCommentText('storybook', storybookResult) + '\n\n---\n' +
              # 複数の Result があったので同様に列挙しました。
              # 共通コメント文章を生成しました。
            let oldComments = await github.rest.issues.listComments({
              issue_number: context.issue.number,
              owner: context.repo.owner,
              repo: context.repo.repo,
            });
            oldComments = oldComments.data.filter((comment) => {
              return comment.body.startsWith(header) && comment.user.login.includes('github-actions');
            });
            await github.rest.issues.createComment({
              issue_number: context.issue.number,
              owner: context.repo.owner,
              repo: context.repo.repo,
              body: commentText
            });
            for (const oldComment of oldComments) {
              await github.rest.issues.deleteComment({
                owner: context.repo.owner,
                repo: context.repo.repo,
                comment_id: oldComment.id,
              });
            }

ちょっと長いですね。

起きた問題と対処

Dependabot

Dependabot の PR では通常の workflow と異なり、fork 扱いで権限が弱くなっています(他者のコードを自動実行するので、言われてみれば当然ですね)。

前段に権限が適切でない場合は skip するような workflow を挟みました。 必要であれば Dependabot の PR を review する人が手動で Rerun します。手動であれば適切な権限で VRT を実行できます。

権限のチェックは以下の step で行った後に

  check-settings:
    runs-on: ubuntu-latest
    outputs:
      result: ${{ steps.configure-aws-credentials.outcome }}
    timeout-minutes: 5
    steps:
      - name: Configure aws credentials
        id: configure-aws-credentials
        uses: aws-actions/configure-aws-credentials@v1
        with:
          role-to-assume: arn:aws:iam::${{ secrets.AWS_ACCOUNT_ID_MAIN }}:role/frontend-staging-playground
          aws-region: ap-northeast-1
        continue-on-error: true

続く step でチェックしています。

  frontend-storybook:
    if: ${{ needs.check-settings.outputs.result == 'success' }}
    runs-on: ubuntu-latest
    timeout-minutes: 15
    needs: [check-settings]
    ...

VRT 差分の過剰検出

当初は placeholder のアイコンを小さめの画像として、拡大して使っていました。 しかし、このアイコンの拡大部分での差が検出され偽陽性となるケースが多くなりました。

対応として大きめの画像を placeholder にして拡大率を下げた結果、この問題は発生しなくなりました。 高周波成分の大きな画像ではやはり問題が起きることが考えられ、ぼやっとした高解像度の画像が良いのかも知れません。

当初、mock ライブラリで数百 ms の delay を入れていましたが、この部分を待ってスクリーンショットを撮る場合と待たずに撮る場合があり、結果が安定しませんでした。

他の選択肢もあると思いますが、今回は VRT では delay は無しとしました。

GitHub Actions の無料枠で間に合わなくなった

弊社では CircleCI をメインで利用しており、GitHub Actions はあまり利用していなかったのですが、今回の導入であっさりと GitHub Actions の無料枠を超えました。

当初から可能性は認識しており、マネージャにその旨のお伺いを立てていましたが、スムーズに従量課金に設定していただけました。

<iframe> で puppeteer 側の例外発生

ほかチームで <iframe> を使用したコンポーネントを実装していた際に、Execution context is not available in detached frame というエラーが発生し、スクリーンショットが撮れない現象が発生していました。

Storycap 開発者からも未知の問題かもと連絡を受けたので、該当の問題はログとして Issue を作成しておきました。

puppetteer では既知の問題として報告・対応されており、バージョンを更新すればこの問題は対応できそうな気もしていますが、一旦該当のコンポーネントのみ Storycap の例外設定にしています。

おわりに

スクリーンショット:GitHub Actions により Storybook の生成と VRT の結果をコメントしてくれているようになった

VRT の導入と GitHub Actions での設定により、プルリクエスト上でレポートされるようになりました!

これで Vue.js で作成したコンポーネントでの CSS リファクタリング心理的安全性が向上し、レビュー時の確認負荷の軽減にも一役買ってくれそうです。

We're Hiring !

クラウドワークスではフロントエンドにとどまらずメンバーを募集しています!

herp.careers

© 2016 CrowdWorks, Inc., All rights reserved.