フロントエンド開発で BackstopJS を使って visual test をする

フロントエンド
フロントエンド

昨今のフロントエンド開発では、コンポーネント志向は当たり前になりました。

コンポーネントと呼ばれる単位で部品が作られ、その積み上げで画面が構成されるイメージです。私は業務システムの開発に携わることが多いですが、そこでも SPA で実現することが増えたように思います。

フロントエンドの設計をする際は、なるべく他のプロジェクトでも使えるよう、Atomic Design などを意識しつつ、以下のように分類して進めることを個人的には心がけています。

  • プロジェクトを超えて汎用的に使える部品
  • プロジェクト固有だけど複数箇所で使う部品
  • プロジェクト固有の一点物(画面)

コンポーネント単位に開発するときに、見た目のチェックに役立つのが BackstopJS です。

Web系の開発をしている方は、もうずっと前から知っていて、高度な使い方をされているかとは思いますが、少なくとも自分のまわりではあまり認知されていませんでした。

簡単に導入でき、レスポンシブにも対応しつつ、影響範囲も確認することができる優れものだなと思いますので、改めてご紹介です。

汎用コンポーネントを作り、複数のプロジェクトでも使いたい。そのような場合にロジックは単体テストで担保、見た目への影響は BackstopJS を使って担保できるのでは、と思います。

Backstopjs とは

GitHub - garris/BackstopJS: Catch CSS curve balls.
Catch CSS curve balls. Contribute to garris/BackstopJS development by creating an account on GitHub.

BackstopJSは、スクリーンショットを比較することにより、レスポンシブWebUIの視覚的な回帰テストを自動化します。

https://github.com/garris/BackstopJS
https://github.com/garris/BackstopJS

簡単に言うと、正しい状態(修正前)を記録しておき、変更後(機能強化やバグFixを実施した後)と比べて、差があるかないか、を自動でチェックしてくれるものです。

また、自動実行するときに、複数の View port も指定できるので、PC用に 1920 x 1080 と タブレット用に別の解像度を、、と1つの対象に対して複数の解像度が指定でき、それぞれの解像度でチェックしてくれます。

インストール

今回は、以下で作った VIte + Vue3 環境で試してみます。

公式にあるように、グローバルでインストールします。以下のみです。

npm install -g backstopjs

初期化

初めての場合は、以下を実行することでデフォルトの設定ファイルなどを生成してくれます。既存の backstop ファイルが有る場合には上書きされてしますのでご注意を。

backstop init

backstop_data と呼ばれるデータ置き場と、backstop.json と呼ばれる設定ファイル(以下)が生成されます。

{
  "id": "backstop_default",
  "viewports": [
    {
      "label": "phone",
      "width": 320,
      "height": 480
    },
    {
      "label": "tablet",
      "width": 1024,
      "height": 768
    }
  ],
  "onBeforeScript": "puppet/onBefore.js",
  "onReadyScript": "puppet/onReady.js",
  "scenarios": [
    {
      "label": "BackstopJS Homepage",
      "cookiePath": "backstop_data/engine_scripts/cookies.json",
      "url": "https://garris.github.io/BackstopJS/",
      "referenceUrl": "",
      "readyEvent": "",
      "readySelector": "",
      "delay": 0,
      "hideSelectors": [],
      "removeSelectors": [],
      "hoverSelector": "",
      "clickSelector": "",
      "postInteractionWait": 0,
      "selectors": [],
      "selectorExpansion": true,
      "expect": 0,
      "misMatchThreshold" : 0.1,
      "requireSameDimensions": true
    }
  ],
  "paths": {
    "bitmaps_reference": "backstop_data/bitmaps_reference",
    "bitmaps_test": "backstop_data/bitmaps_test",
    "engine_scripts": "backstop_data/engine_scripts",
    "html_report": "backstop_data/html_report",
    "ci_report": "backstop_data/ci_report"
  },
  "report": ["browser"],
  "engine": "puppeteer",
  "engineOptions": {
    "args": ["--no-sandbox"]
  },
  "asyncCaptureLimit": 5,
  "asyncCompareLimit": 50,
  "debug": false,
  "debugWindow": false
}

実行(テスト)- backstop test

以下のコマンドで実行できます。

backstop test

バージョンによっては、実行時に以下のエラーが発生する可能性があります。

Error [ERR_REQUIRE_ESM]: require() of ES Module /vite-storybook/backstop_data/engine_scripts/puppet/onReady.js from ~.nvm/versions/node/v16.14.2/lib/node_modules/backstopjs/core/util/runPuppet.js not supported.
onReady.js is treated as an ES module file as it is a .js file whose nearest parent package.json contains "type": "module" which declares all .js files in that package scope as ES modules.
Instead rename onReady.js to end in .cjs, change the requiring code to use dynamic import() which is available in all CommonJS modules, or change "type": "module" to "type": "commonjs" in /vite-storybook/package.json to treat all .js files as CommonJS (using .mjs for all ES modules instead).

その際は、以下のファイルの拡張子を 「.cjs」に変更して再度実行すると動作します。

  • backstop_data/engine_scripts/puppet/onBefore.js
  • backstop_data/engine_scripts/puppet/onReady.js

成功すると、以下のような結果がでます。今は正解画像(左の REFERENCE)がないため空、今回のテスト対象(中央の TEST)のみ表示されています。

BackStop 実行結果
BackStop 実行結果

実行(テスト) – コンポーネント

上の例は、backstopjs のサイトでしたが、今回は、Storybook の Story をテストさせてみます。Storybook は iframe.html を利用することで、コンポーネント自身だけをページに表示させることもできます。そのURLを使うことで純粋のコンポーネントのみをテストできます。追加したのはハイライト部分です。

Storybook で元々あるヘッダです。

// backstop.json
{
  // ... 他のもとの記述

  // シナリオ
  "scenarios": [
    { ... 上に記載した BackstopJS Homepage のテスト },
    {
      "label": "Header",
      "url": "http://localhost:6006/iframe.html?args=&id=example-header--logged-in&viewMode=story",
      "delay": 3000
    }
  ],
  // 他のもとの記述
}

Storybook を起動して再度実行します

npm run storybook
backstop test
BackStop 実行結果2
BackStop 実行結果2

表示されました。

正しい結果として登録 – backstop approve

現在の TEST 結果が問題なければ、以下のコマンドを実行します。

backstop approve

これで、正しい結果として登録されました(すでに正しい結果がある場合は更新されます)。

試しに、表示を少し変えて backstop test を行ってみます。

左が先程登録した画像、中央が現在の結果、右が差分です。違いが検出されました。

BackStop 実行結果3 - 正誤の結果を重ねる
BackStop 実行結果3 – 正誤の結果を重ねる

差分の画像をクリックするとより具体的に見ることができます。SCRUBBERは赤線がドラッグでき、赤線境に REFERENCEとTESTの結果が表示されます。

今回は、padding の変更と、Welcome の文言を変更しています。

BackStop 実行結果4 - 正誤の比較
BackStop 実行結果4 – 正誤の比較

レンダリングエンジンの変更

BackstopJS は、レンダリングエンジンとして puppeteer が使用されていました(chromium)が、v6.0.1 から Playwright エンジンを搭載したようです(なので npm install に時間がかかっていたのか・・・)。

そのため、エンジンを変えると、Firefox や webkit で実行させることもできます(デフォルトは puppeteer です)。変更方法も簡単で、以下のように、engineplaywright にして、engineOptionsbrowserwebkitfirefox にして、実行するだけです。

// backstop.json

{  
  ...
  "engine": "playwright",
  "engineOptions": {
    "browser": "webkit"
  },
  ///
}

まとめ

筆者自身、正直十分には活用しきれている、とは言い難いですが、この記事で書いたように、最低限 Storybook と組み合わせることで、画面単位だけでなく、コンポーネント単位で見栄えを確認し、意図していないコンポーネントに影響していないか、ということをしています。

この程度であれば導入もすぐにできるので、コンポーネントのコードやスタイルを変更する際には、品質担保の仕組みの1つとして試してみるとよいかなと思います。

コメント