Playwright & Vite ではじめる脱レガシー向け軽量 Visual Regression Test

こんにちは!フロントエンドエキスパートチームの @mugi_uno です。

みなさんは Visual Regression Test は普段活用していますか?

昨今では事例もよく耳にするようになった印象です。一度使って手放せなくなった方もいるのではないでしょうか。

今回の記事では、通常のプロダクト新規開発とは異なる “脱レガシー” の文脈で Playwright を用いた簡易的な Visual Regression Test を試してみたので、導入に至る経緯と、どのように実施しているかを紹介します。

フロントエンドリアーキテクチャとサイレントリリース

現在サイボウズでは kintone のフロントエンドリアーキテクチャプロジェクトと称して、Closure Tools から React への脱レガシー作業が進行中です。プロジェクトの詳細については @koba04 が書いた次の記事をご覧ください。

blog.cybozu.io

一口にフロントエンドリアーキテクチャといっても、kintone は規模が大きく、さまざまな切り口から施策・検証を行っている段階です。

その中のひとつのチームで、サイレントリリース を部分的に試みています。

ここでいうサイレントリリースとは、ユーザーから見たときのふるまいは変えずに既存実装を React に置き換えていくことを指します。

※ 余談ですがチーム名は "Mira" です。星言葉で 「安定感ある穏やかさ」という意味があるそうです。オシャレですね。

ユーザー視点では「えっ?これもう React に変わってたの?」となるのが理想です。これは見た目に関しても同様で、チームでは "置き換え前後で見た目を変更しない" を前提に作業を進めています。

見た目を揃えることで意思決定を減らす

React への置き換え中に「この部分のデザインを直したいなぁ…」と感じる部分が出てくることもありますが、実際に変更するとなると次のような点をクリアにしなければなりません。

  • 本当に変更が必要かの調査(意図したデザインではないのか?)
  • 変更後デザインの検討
  • a11y サポートの設計
  • ロケールに応じた文言の検討

脱レガシー作業時は、できる限り複数のことを並行して対応せず、実現したい事柄のひとつずつにフォーカスしての対処が重要になります。並列するほどに関係者の数が増え意思決定コストも膨らみ、結果としてリリースまでのコストが大きくなります。

大前提を "見た目は変えない" としてデザイン面での判断を大きく削ることで、React 化そのものに集中できます。

Visual Regression Test

既存 CSS を再利用することで見た目を再現していく方針で作業を進めていますが、React 化に伴う実装の都合上 DOM 構造まで完全に一致させることは難しく、伴って一部 CSS は書き直す必要が生じます。それにより、注意していても意図しない見た目の差分が発生する恐れがあります。

そこで Visual Regression Test (VRT) です。

目視での見た目の差分チェックには限界がありますが、VRT では実際の画面スナップショットを元に機械的に画像差分を検知するため、見落としを限りなく減らすことができます。

手法自体は珍しいものではなく、検索すれば事例も多くヒットします。

一般的な VRT との違い

一般的に VRT と言われてイメージするのは次のような形が多いかと思います。

  • 特定の一つの環境に対してスクリーンショットを撮影して保存
  • コードの変更時に、同環境・同フローで撮影して保存画像と比較
  • 差分の有無に応じて、意図した変更かを目視で確認する
  • 撮影した画像を次回利用のため保存

一般的な VRT では同一環境でのスナップショットを継続的に取得し、変更のたびに都度前回との比較を行います。また、差分を検知しても変更後の内容が正しいケースがあるため、最終的な判断は人間が目視で行う必要があります。撮影には専用の検証環境であったり、Storybook が用いられるケースもあります。

一般的なVRTのイメージ
一般的なVRTのイメージ

一方で、今回のサイレントな React への置き換えで実現したい VRT は次のようなものです。

  • レガシーコードを含む現行環境でのスクリーンショットを撮影
  • コードの変更時に、現行環境での撮影画像と React 環境での撮影画像を比較
  • 差分があった場合は React 側を修正する

大きな違いとしては、最新の現行環境での見た目が常に正解として存在する点です。基本的に差分検知時はすべて React 環境側が誤っているものとして取り扱います。撮影対象も現行環境(Closure Tools)・React 環境の2つが対象となります。

(※ ひとつの画面内で部品単位で置き換え&都度リリースが可能なフローであれば、単一環境での撮影でも可能となります。このあたりはプロダクトやチームのプロセスに左右される部分かと思います)

f:id:cybozuinsideout:20220317133430j:plain
kintone React化で実現したいVRTイメージ

Playwright で軽量に VRT をはじめる

先に挙げたような内容から、一般的な VRT とは少し違う条件のもとで環境を構築する必要があります。

また、脱レガシー中に利用するものなので、刷新完了後には、より運用しやすい理想的な形での VRT への置き換えも考えられ、すべて捨てられる可能性もあります。そのため、あまりコストをかけずにお手軽に導入をしたいところです。

というわけで、少し前置きが長くなりましたが Playwright & Vite を利用した簡易的な VRT のご紹介です。

※ 実際にコードを見たい方向けに、最小限のサンプルコードを用意しました。興味があれば併せてどうぞ!

github.com

toMatchSnapshot

Playwright は Microsoft 製のブラウザの自動化&テスト用フレームワークです。Cypress や Selenium と並べて比較されることも多く、最近名前を聞く機会が増えてきたように感じます。

playwright.dev

Playwright には toMatchSnapshotという API が存在し、デフォルトで画像ベースでのスナップショットテストをサポートしています。

たとえばテストコード上に

expect(await page.screenshot()).toMatchSnapshot('image.png');

と記述しておくだけで、

  • 画面のスナップショットの取得
  • 保存されている image.png との比較
  • 差分検知時は確認用の画像を出力

をすべて行うことができます。

Jest でのスナップショットテストの画像比較版のようなイメージと言えばピンとくる方もいるかもしれません。*1

現行環境のスナップショット取得

Playwright を利用し、まずは 現行環境を対象に toMatchSnapshot を含むテストを作成します。

対象は本番でも良いですし、撮影用環境を専用に立てても良いでしょう。今回のケースでは、撮影専用に現行環境を用意して対処しました。

コードは次のようなイメージです。

test('Visual Regression Test', async ({ page }) => {
  // 撮影用環境に遷移する
  await page.goto('https://example.com/xxx');>

  // 撮影準備のため任意の操作を行う
  await page.fill('.input', '入力値');

  expect(await page.screenshot()).toMatchSnapshot('vrt.png', { threshold: 0.075 });
});

撮影する画像には少しノイズが乗ることがあり、100%の画像一致で比較すると fail するため、threshold で少しだけ精度を調整しています。

ちなみに、当初チームの @nus3threshold に 0.1 を設定していましたが、さらに細かく調整しようと言った結果 VRT 職人を襲名しました。

VRT精度のギリギリを攻める人たち
VRT 精度のギリギリを攻める人たち

VRT職人襲名
VRT 職人襲名

これで

npx playwright test --update-snapshots

を実行すれば、期待結果とするスナップショットの取得が可能になりました。

React 環境 → Vite と組み合わせて撮影

次に、React 化後の環境で撮影したものと比較するため、そちらも何らかの形で撮影環境を用意する必要があります。

突然ですが、React へのサイレント置き換えの実装では、ビルドに Vite を利用しています。

ja.vitejs.dev

kintone は MPA 構成のため、JS をエントリーポイントにビルドしています。通常のビルド時の vite.config.js の設定は次のような形となります。

vite.config.js

export const config = {
  build: {
    rollupOptions: {
      input: {
        'app': path.resolve(__dirname, 'src/app.tsx')
      }
    },
  ...

しかし、Vite は本来 HTML をエントリーポイントに取ることができます。これを利用して、VRT 向けのビルド設定を簡略化し、一気に撮影用のサーバー起動まで行うことができます。

例として、次のような HTML を用意します。

src/index.html

<!DOCTYPE html>
<html class="modern" lang="ja">
  <head>
    <meta charset="UTF-8" />
    <meta
      name="viewport"
      content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0"
    />
    <title>VRT</title>
  </head>
  <body>
    <div id="app"></div>
    <script src="./app.tsx" type="module"></script>
  </body>
</html>

そして、専用の Vite 設定ファイルを用意します。

vite.vrt.config.js

export const config = {
  build: {
    rollupOptions: {
      input: {
        'app': path.resolve(__dirname, 'src/index.html')
      }
    },
  ...

この状態で vite serve --config vite.vrt.config.ts で開発サーバを起動すると、 アクセスした時点で Vite の仕組みにより Native ESM を利用しビルドした結果が返されます。バックエンドとの通信が必要なケースでは、Playwright の Network mocking*2 を利用して適宜モック化します。

これにより、React 化後の撮影用環境を簡易的に用意でき、かつビルド設定も必要最低限で VRT を実施することができます。

Playwright の実行に併せて Vite を起動する必要がありますが、これも Playwright 設定の webServer を利用すれば自動化できます。

playwright.config.ts

const config: PlaywrightTestConfig = {
  webServer: {
    command: 'npx vite serve --config vite.vrt.config.ts,
    port: 3000,
    reuseExistingServer: true
  },
  ...

テスト内では環境変数に応じてアクセスの一部を切り替えます。

test('Visual Regression Test', async ({ page }) => {
  if (process.env.REACT) {
    // 変更後環境に遷移する
    await page.goto('http://localhost:3000/src/');>
  } else {
    // 変更前環境に遷移する
    await page.goto('https://example.com/xxx');>
  }

  // 撮影準備のため任意の操作を行う
  await page.fill('.input', '入力値');

  expect(await page.screenshot()).toMatchSnapshot('vrt.png', { threshold: 0.075 });
});

これで簡易的な VRT が動くようになりました。

  • npx playwright test --update-snapshots → 変更前環境でスナップショットを取得
  • REACT=1 npx playwright test → 変更後環境(Vite)で撮影した画像で差分を比較

サンプルコードを対象に意図的にわずかな余白の差異のみを埋め込んで実行すると、次のようなアウトプットを得られます。

差分検知時のアウトプット
差分検知時のアウトプット

3ファイル出力され、上から 現行環境・React環境・Diff イメージ の順で並べてあります。

目視では見落としそうな差分ですが、ちゃんと余白の違いを認識して検知できていますね。

最終的なイメージ

ここまでの内容をベースに、実際の開発時には CI (GitHub Action) での実行や、環境差異を無くすための Docker 起動などもセットアップしています。

全体を整理すると次のようなイメージとなります。

最終的なVRTのイメージ
最終的なVRTのイメージ

Pros/Cons

今回ご紹介した Playwright & Vite を利用した脱レガシー文脈での VRT の Pros/Cons を考えてみます。

👍 Pros: 手元で高速に実行できる

「既存のデザインをそのまま再現する」という作業の都合上、開発時には高い頻度で VRT を実行したくなります。

具体的には、

  • 手元でコードを変更する
  • VRT で差分を確認
  • 違いがある箇所を直す

といったサイクルで開発を進めたいところですが、都度 React 環境をビルドしてどこかへデプロイして CI で実行して… という手順が必要だと、開発効率に大きく影響を与えます。

Vite でローカルに確認用サーバーを都度立ち上げることで、環境準備のオーバーヘッドが非常に小さくなります。単純に Vite でのビルド自体も非常に高速なのも嬉しいところです。

👍 Pros: 外部インフラが最小限で済む

React 環境の撮影はローカルで閉じるため、外部環境への依存を減らせます。

複数人が撮影用環境を触ったことでスクリーンショットの内容が意図せず変わってしまったり、環境自体に問題がありアクセスできなかったりといった課題を回避できます。

外部環境ではビルド&デプロイの設定や管理も必要となりますが、そのあたりをまるっと Vite が吸収できるのも楽な点です。

👍 Pros: 画像の保存を省略できる

通常 VRT では、スナップショットの画像をどこかに保存しておく必要があります。しかし、今回のパターンでは常に最新の現行環境での見た目を正として良いため、テストのたびに都度撮影する形で問題なく、画像の保存自体を省略する選択肢を取れます。別の VRT ツールである reg-suit などでは AWS S3 に画像を保存するケースが多いかと思いますが、そのあたりのストレージ管理を考える必要がなくなり、手間が減ります。

🙈 Cons: 大量の差分確認時は手間がかかる

Playwright の toMatchSnapshot では変更を検知した際に、変更差分がわかるような画像を出力してくれますが、1 ファイルずつ開いて内容を確認していく必要があります。

対象画面が少なければ特に問題にはなりませんが、多くのスクリーンショットが発生するケースでは reg-suit のように確認用のページを出力してくれるほうが便利かもしれません。

🙈 Cons: 場合によっては API のモック化が必要

そもそも API が必要ない or テスト用の API サーバが用意できれば問題ないですが、そうでなければ Vite 環境での撮影時にはバックエンド側をモック化する必要があります。

今回のケースでは、Integration Test といった他テストで外部データ層は既にモック化しており、そのまま流用可能なためそれほどコストにはなりませんでしたが、モックを是とするかはチームのポリシーにもよるかと思います。好まない場合には何らかの対策を講じる必要が生じます。


というわけで、脱レガシー文脈での Playwright & Vite を利用した VRT のご紹介でした。

常に導入可能なパターンではないかもしれませんが、シチュエーションによっては非常に刺さり、かつ低コストで導入可能なので、興味があればぜひお試しください。