Visual Regression Testing はじめました - 具体的な運用 Tips

kazuma1989
29

こんにちは。スタディサプリ ENGLISH Web フロントチームの kazuma1989 です。

先日、私たちのチームは開発フローに Visual Regression Testing を導入しました。Visual Regression Testing は、フレームワークを紹介する記事は見つかるものの、具体的な知見があまり広まっていない印象なので、具体的な設定値や選定理由も含め紹介してみます。

React による Web フロント開発を前提にしていますが、Visual Regression Testing のコア部分は「画像の比較」であるため、他のプラットフォーム開発でも参考になればと思います。

Visual Regression Testing (VRT) とは

Visual Regression Testing (日本語で 画像回帰テスト、以下 VRT)は、画像の差分を検出する、スナップショットテストのひとつです。ほかのスナップショットテストとして、Jest による、DOM やオブジェクトを対象としたテストがあります。VRT はその対象をスクリーンショット画像、つまり UI をブラウザーで表示したときの見た目としています。

テストレポートは次のようなものです。画像の差分が強調され、変更箇所がひと目でわかります。

VRT を導入するモチベーションは、改修による予期せぬ UI のデザイン崩れ(いわゆるデグレ)を素早く見つけることにあります。たとえば Button コンポーネントなどはあらゆる画面で使われますが、その改修の影響をすべて手動で検査するのは不可能なため、ツールに大量のスクリーンショット画像を採取・比較させるのです。

当然、UI の見た目が変わらないまま期待どおり動作しなくなるケースもあり得ます。VRT はあくまでテストのひとつであり、動作面は手動の動作確認や End-to-End (E2E) テスト(cf. TestCafe で E2E テストを始めよう #1 – 概要説明 と Hello World – PSYENCE:MEDIA)などで担保する必要があります。しかし、ときに JSON 色つけ係 と形容されるように、Web フロントの実装は表示さえできれば正常動作を期待できる部分が多いのも事実です。VRT で大部分の UI を守りながら、E2E テストや手動テストによってとくに重要な小量の UI を守るというのは、悪くない戦略です。

VRT のツールセット

スタディサプリ ENGLISH Web フロントの VRT では以下のツールを使っています。

  • Storybook. UI カタログ。スクリーンショットの対象とします。
  • Storycap. Headless Chrome を使い Storybook のスクリーンショット画像を採取してくれます。
  • reg-suit. 画像の差分をレポートとして出力してくれます。

各ツールの関係は次の図のとおりです(CI として CircleCI を使っています)。

Storybook で UI のプレビューを表示し、そのプレビューから Storycap アドオンによってスクリーンショット画像をコンテナローカルに生成します。reg-suit は、コンテナローカルのスクリーンショット画像と AWS S3 に保存済みの画像を比較し、その結果をレポートとして AWS S3 に保存します。レポートと同時に、コンテナローカルのスクリーンショット(actual だったもの)を次の期待値として保存します。関連するプルリクエストがある場合はそこにレポート概要をコメントします。

プルリクエストへのコメントは次のようなものです。

ツールの選定理由

Storybook

Web の UI カタログとして VRT 導入前から使っていたものです。ほとんどデファクトスタンダードなので採用しています。代替ツールはあまり知らないのですが、React に限れば React Cosmos が使えるかもしれません。

UI カタログを使わなくとも、webpack-dev-server などの開発サーバーで代替してもよいでしょう。ただしその場合、スクリーンショットを採取するには目的の画面に至るまでのコードを自分で書く必要があるので、カタログのメンテナンスをするのと大変さは変わらないかもしれません。

Storycap

Storybook のアドオンであり、スクリーンショット画像を採取する CLI ツールでもあります。Storybook のストーリーごと、もしくはビューポートごとに画像を生成してくれます。似たようなツールに storybook-chrome-screenshotzisui がありますが、これらの後継である Storycap を採用しています。

cf. https://quramy.medium.com/storybook-chrome-screenshot と zisui と storycap と-b878f8ed8361

Storybook を使わなかった場合、Puppeteer, Playwright などを使って画面のスクリーンショットを採取します。

reg-suit

VRT のコアとなる「画像の比較」を行ってくれます。以下のプラグインを組み合わせて使っています。

VRT には多くのツールがありますが、画像の比較だけに機能を絞ったものは意外と多くないようです。たとえば BackstopJSJest-Image-Snapshot はスクリーンショットの撮影も担っています。単機能なツールとオールインワンなツールはどちらも一長一短ですが、次の利点を考えて、画像を比較するだけのツールを選んでいます。

  • 自動でスクリーンショットを撮りづらい UI も、何らかの方法で画像さえ用意できれば VRT の対象にできる。
  • スクリーンショット画像自体が、VRT をしなくても UI カタログとして参考になる。
  • スクリーンショット画像はコミットごとに不変な一方、差分レポートは比較対象の選び方で変わってくる。ツールが別れていれば、その差を吸収して柔軟な運用が設計できる。

現状、手動で画像を用意して VRT するなどの運用は、実際にはしていません。将来的な変更を見据えて柔軟なほうを選びました。柔軟さのトレードオフであるセットアップの大変さもそこまで大きくありませんでした。

CircleCI

CI としてもともと CircleCI を使っていたので VRT でも使っています。VRT の実行環境は、Headless Chrome 入りの cimg/node というイメージをもとにしたコンテナです。同じようなイメージを用意すれば GitHub Actions でも代替できると思いますし、開発者の端末でもやはり VRT は可能です。

ちなみに、私たちのチームは CircleCI にある程度自前の仕組みを構築しましたが、PercyChromatic といったオールインワンのサービスもあるようです。

VRT の設定まわりやカタログの工夫

reg-suit の具体的な設定値や、VRT しやすいカタログの書き方、レビューでの活用方法を紹介します。

VRT の設定まわり

差分閾値は 0.1% (core.thresholdRate = 0.001)

一般的に何かを検査すると false positive または false negative が結果に含まれますが、VRT では差分閾値がその割合を左右する重要なパラメーターです。問題がないのに差分ありと報告されたくない、つまり false positive を少なくしたいときは閾値を大きくします。逆に、なるべく差分を見落としたくない、つまり false negative を減らしたい場合は閾値を小さくします。

私たちのチームでは 0.1%、つまり 32x32 のメッシュの 1 箇所以上で差分があれば検出するようにしています。カタログの規模的に false positive を目視でチェックしきれるので、なるべく false negative を出さないであろう値にしています。今後の運用で見直すかもしれません。

テスト結果をプルリクエストの CI ステータスにあえて反映しない (plugins.reg-notify-github-plugin.setCommitStatus = false)

プルリクエストに対する CI の一環として VRT を実行していますが、その結果をあえてプルリクエストのステータスとして反映していません。コメントによってレポートを通知するのみです。これは、次の理由から結局目視で妥当性の判断をすることが多かったためです。

  • False positive とわかっても、コード修正によって是正しようのないことがある。
  • 差分が意図どおりのことがある。

また、プルリクエストを承認すると VRT のステータスがパス状態に変更されるという挙動も、CI ステータスへの反映をやめた理由です。VRT のステータスをレビュアーがパス状態に変更する、つまり VRT の内容確認の責務をレビュアーが負うのは、VRT 自体が発展途上な運用のため、過剰と判断したのです。かといってプルリクエスト作成者が責務を負うべきかというのも、チーム内でうまく決めることができませんでした(false positive もあるため)。

私たちのチームでは、画面キャプチャの画像や動画をプルリクエストへ添付することを心がけていますが、VRT はその補助や延長として使うようにしています。

日本語フォント不在による豆腐(文字化け)はあえて残す

CircleCI のイメージ cimg/node には日本語フォントが含まれておらず、デフォルトのフォント指定では日本語は 豆腐 (文字化け)となってスクリーンショットが作られます。スタディサプリ ENGLISH ではすべてを Web フォントとしており、この問題が起きないはずなのですが、まれに意図どおりフォントが指定されていないことがあります(洗い出しと対応が今後の課題です)。これを検知するため、あえて日本語フォントをインストールしたイメージは使っていません

カタログの工夫

カタログを書きやすくするグローバル型を定義

VRT はカタログが充実するほど生きるので、なるべくカタログが書きやすくなるよう工夫しています。Storybook でカタログを書く際、メタ情報(カタログ名やビューポート、コンポーネントのプロパティなど)は default export として書きますが、その型をアンビエントに定義してすぐ参照できるようにしています。

// as StorybookMeta としておくと、オブジェクト内の補完が効いて書きやすくなる
export default {
  title: "path/to/HomeMobile",
  component: HomeMobile,
  parameters: {
    viewport: {
      defaultViewport: "iphone6",
    },
    screenshot: {
      viewport: "iPhone 6",
    },
  },
} as StorybookMeta;

API モックに Mock Service Worker を使う

Mock Service Worker を使い、クライアント側のコードだけで API をモックしています。実際の XHR 通信をモックできるため、アプリケーションコードに細工が不要な点が特徴です。

API モックの必要性は、コンポーネント設計やカタログの粒度により変わります。スタディサプリ ENGLISH Web 版のコンポーネントは、画面固有のものも多いため、適度にロジックや API 呼び出しを内包したものとなっています。そのため、カタログの粒度も画面単位かそれに近い状態となっていて、API モックがあると便利なのです。

VRT を始めて得たものとこれからの課題

スタディサプリ ENGLISH の VRT は始まって 2 か月弱ですが、すでにいくつかの効果をもたらしています。

  • 豆腐フォントによって CSS の適用漏れが見つかった
  • フォントが豆腐になったりならなかったりするコンポーネントがきっかけでフォントファイルが見直され、フォントファイルのサイズを減らすことができた(豆腐になったりならなかったりする事象の根本解決にはならなかったものの)。
  • アクセシビリティ向上のため色の整理が進んでいるが、その変更の検証をある程度自動化できた
  • カタログの書きづらさから、インターフェースが過剰なコンポーネントに気づくことができた(API のレスポンスを丸ごと受け取り、表示には使わない値も要求していた)。

当面の課題は次の 3 点です。

  • 一部カタログで、タイミングによってフォントが豆腐になったり、アニメーションによって差分を生じたりする(タイムアウトで失敗することもある)ので、改善が必要。
  • カタログがまだまだ少ないので、充実させる必要がある。
  • プルリクエストに対する VRT の重みづけ(レビュアー/レビュイーの責任範囲、テストパスを必須条件とするか)を見直す必要がある。VRT 結果を重視する/しないが人によって変わるのは運用形態として望ましくないため。

VRT は、現状物量や品質そして運用に改善の余地がありますが、実施のコスト(カタログを書いたり CI 完了を待ったり)は問題にはなっていません。Web フロントの自動テストの中でも、ユニットテストや E2E テストと比べ、費用対効果の見えやすいテストなのではないでしょうか。

この記事が、VRT 導入の参考に、そしてさらなる知見の共有につながれば嬉しいです。

参考文献