経緯
Jest でユニットテストができることは分かりました。何かを処理するものに関しては行けそうな気がします。
……しかし、Webサイトの場合は見た目も気になります。見た目をテストするにはどううれば良いのか……と考えていたところ、 BackstopJS というものがあることを知ったので試してみることにします。
準備
見た目のテストのためには何かページを生成するものが必要です。
今回は簡単に試せる環境として、手前味噌ですが arm-band/mori を使うことにしました。
1. パッケージ追加
package.json
に backstopjs
を追加します。
// 略
"devDependencies": {
"backstopjs": "^5.0.6", // 追加
"browser-sync": "^2.26.12",
// 略
},
// 略
2. yarn
これで yarn
してインストールを待ちます。
> yarn
BackstopJS の準備
後は手順通りに backstop init
して
> backstop init
生成された backstop.json
の scenarios
-> url
を編集。今回は Browsersync のローカルサーバにしたいので https://localhost:3000/
で。
// 略
"scenarios": [
{
"label": "BackstopJS Homepage",
"cookiePath": "backstop_data/engine_scripts/cookies.json",
// "url": "https://garris.github.io/BackstopJS/",
"url": "https://localhost:3000/", // 変更
"referenceUrl": "",
// 略
}
]
// 略
検証1 (失敗)
ここまで準備をしたら、正解の見本を保存するために backstop reference
でスクリーンショットを撮ります。
まずは yarn start
で Gulpタスク を走らせて https://localhost:3000/
を上げます。
次に、上述の backstop reference
コマンドを実行します。
> backstop reference
BackstopJS v5.0.6
Loading config: PATH\TO\TEST_PROJECT\randa_back_stopper\backstop.json
COMMAND | Executing core for "reference"
clean | backstop_data/bitmaps_reference was cleaned.
createBitmaps | Selected 1 of 1 scenarios.
COMMAND | Command "reference" ended with an error
after [1.414s]
Error: Failed to launch the browser process!
TROUBLESHOOTING: https://github.com/puppeteer/puppeteer/blob/master/docs/troubleshooting.md
at onClose (PATH\TO\TEST_PROJECT\randa_back_stopper\node_modules\puppeteer\lib\Launcher.js:750:14)
at ChildProcess.<anonymous> (PATH\TO\TEST_PROJECT\randa_back_stopper\node_modules\puppeteer\lib\Launcher.js:740:61)
at ChildProcess.emit (events.js:326:22)
at Process.ChildProcess._handle.onexit (internal/child_process.js:276:12)
# 略
……あれ?失敗しました。
Command “reference” ended with an error after
Error: Failed to launch the browser process!
エラーメッセージ的にはこの部分が原因っぽいですね。
そして、問題を起こしているのは puppeteer
のようです。
ちなみに、 BackstopJS がスクリーンショットを撮る際は Puppeteer からヘッドレスブラウザとして Chromium でレンダリングしている模様。
ということで、原因は Puppeteer にあり。エラー文で検索しましたが、イマイチ情報が少ない上にあまり新しい情報がないので、半ば手探りで追及してみることになりました……。
Puppeteer の検証1
まずはそもそも Puppeteer が自分の環境で動くか確認してみます。
> node -v
v14.8.0
> npm -v
6.14.7
Node.js とnpm のバージョン確認。
この状態で新規のプロジェクトを作成します。
package.json
{
"name": "puppeteer_test",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "",
"license": "ISC",
"devDependencies": {
"puppeteer": "^5.5.0"
}
}
npm init
で package.json
を用意して、 yarn add --dev puppeteer
で devDependencies
に追加します。
index.js
const puppeteer = require('puppeteer');
(async () => {
const browser = await puppeteer.launch();
const page = await browser.newPage();
await page.goto('https://google.com/');
await page.screenshot({path: 'screenshot.png'});
await browser.close();
})();
Google のスクリーンショットを撮影するだけの簡単な JS を用意します。
これで
> yarn
# 略
> node index.js
>
……普通に撮影でき、 screenshot.png
が保存されました。問題ありません。
Puppeteer の検証2
次に、 BackstopJS の内部で使用されている Puppeteer のバージョンを確認するために node_modules/backstopjs/package.json
を調べます。
{
"name": "backstopjs",
"version": "5.0.6",
"description": "BackstopJS: Catch CSS curveballs.",
"bin": {
"backstop": "./cli/index.js"
},
"scripts": {
"lint": "eslint 'compare/*.js' 'compare/src/**/*.js' 'core/**/*.js' 'cli/**/*.js' 'capture/**/*.js' 'test/**/*.js'",
"format": "prettier-eslint --write \"compare/src/**/*.js\"",
"genConfig": "node ./cli/index.js genConfig",
"init": "node ./cli/index.js init",
"reference": "node ./cli/index.js reference",
"test": "node ./cli/index.js test",
"approve": "node ./cli/index.js approve",
"openReport": "node ./cli/index.js openReport",
"report": "node ./cli/index.js openReport",
"echo": "node ./cli/index.js echo",
"unit-test": "mocha --reporter spec --recursive 'test/**/*_spec.js'",
"precommit": "lint-staged",
"build-compare": "cp ./node_modules/diverged/src/diverged.js ./compare/output/ && cp ./node_modules/diff/dist/diff.js ./compare/output/ && webpack --config ./compare/webpack.config.js && npm run lint",
"dev-compare": "webpack-dev-server --content-base ./compare/output --config ./compare/webpack.config.js",
"integration-test": "rm -rf newdir && mkdir newdir && cd newdir && node ../cli/index.js genConfig && node ../cli/index.js reference && node ../cli/index.js test && node -e \"require('../')('test')\"",
"smoke-test": "cd test/configs/ && node ../../cli/index.js test --config=backstop_features",
"smoke-test-docker": "cd test/configs/ && node ../../cli/index.js test --config=backstop_features --docker",
"sanity-test": "cd test/configs/ && node ../../cli/index.js test",
"sanity-test-docker": "cd test/configs/ && node ../../cli/index.js test --docker",
"kill-zombies": "pkill -f \"(chrome)?(--headless)\"",
"copy-report-bundle": "mkdir -p test/configs/backstop_data/html_report && cp compare/output/index_bundle.js test/configs/backstop_data/html_report/",
"build-and-copy-report-bundle": "npm run build-compare && npm run copy-report-bundle",
"remote": "node ./cli/index.js remote",
"stop": "curl http://localhost:3000/stop"
},
"lint-staged": {
"compare/src/**/*.js": [
"node_modules/.bin/prettier --single-quote --write",
"git add"
]
},
"repository": {
"type": "git",
"url": "https://github.com/garris/backstopjs.git"
},
"author": "https://github.com/garris/BackstopJS/graphs/contributors",
"license": "MIT",
"main": "core/runner.js",
"devDependencies": {
"assert": "^1.4.1",
"babel-core": "^6.26.3",
"babel-loader": "^7.1.5",
"babel-preset-es2015": "^6.24.1",
"babel-preset-react": "^6.24.1",
"backstop-twentytwenty": "^1.0.4",
"eslint": "^5.6.1",
"eslint-config-semistandard": "^12.0.1",
"eslint-config-standard": "^12.0.0",
"eslint-plugin-import": "^2.14.0",
"eslint-plugin-node": "^7.0.1",
"eslint-plugin-promise": "^4.0.1",
"eslint-plugin-react": "^7.11.1",
"eslint-plugin-standard": "^4.0.0",
"file-loader": "^2.0.0",
"lint-staged": "^4.3.0",
"mocha": "^5.2.0",
"mockery": "^1.4.0",
"prettier": "^1.14.3",
"prettier-eslint-cli": "^4.7.1",
"react": "^15.6.1",
"react-dom": "^15.6.1",
"react-modal": "^3.0.3",
"react-redux": "^5.0.6",
"react-sticky": "^6.0.1",
"react-toggle-button": "^2.1.0",
"react-visibility-sensor": "^3.11.1",
"redux": "^3.7.2",
"sinon": "^1.17.7",
"styled-components": "^2.1.2",
"url-loader": "^1.1.1",
"webpack": "^4.43.0",
"webpack-cli": "^3.1.2",
"webpack-dev-server": "^3.3.1"
},
"dependencies": {
"chalk": "^1.1.3",
"diverged": "^0.1.3",
"fs-extra": "^0.30.0",
"jump.js": "^1.0.2",
"junit-report-builder": "^1.3.3",
"lodash": "^4.17.11",
"merge-img": "^2.1.3",
"minimist": "^1.2.0",
"node-resemble-js": "^0.2.0",
"object-hash": "1.1.5",
"opn": "^5.3.0",
"os": "^0.1.1",
"p-map": "^1.1.1",
"path": "^0.12.7",
"portfinder": "^1.0.17",
"puppeteer": "^2.0.0",
"resolve": "^1.11.1",
"super-simple-web-server": "^1.1.2",
"temp": "^0.8.3"
}
}
puppeteer
のバージョンは…… 2.0.0
?!先ほど入れた最新版は 5.5.0
だったのでかなりバージョンに乖離があります。
ということで先程の Puppeteer のテストプロジェクトの package.json
を直します。
{
"name": "puppeteer_test",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "",
"license": "ISC",
"devDependencies": {
"puppeteer": "^2.0.0"
}
}
はい。 2.0.0
です。これで yarn
し直して node index.js
を実行。
> node index.js
(node:28172) UnhandledPromiseRejectionWarning: Error: Failed to launch the browser process!
TROUBLESHOOTING: https://github.com/puppeteer/puppeteer/blob/master/docs/troubleshooting.md
at onClose (PATH\TO\TEST_PROJECT\puppeteer_test\node_modules\puppeteer\lib\Launcher.js:750:14)
at ChildProcess.<anonymous> (PATH\TO\TEST_PROJECT\puppeteer_test\node_modules\puppeteer\lib\Launcher.js:740:61)
at ChildProcess.emit (events.js:326:22)
at Process.ChildProcess._handle.onexit (internal/child_process.js:276:12)
Error: Failed to launch the browser process!
のエラーが出て、 BackstopJS のプロジェクトで出た内容を再現できました。
エラーの発生個所も node_modules\puppeteer\lib\Launcher.js:750:14
以下同じです。
念のための確認
エラー文の中にあるトラブルシューティングなど、いくつかの記事で「 puppeteer.launch()
のオプションで ignoreDefaultArgs: ['--disable-extensions']
を指定すると良い」と書かれていたので、一応やってみます。
const puppeteer = require('puppeteer');
(async () => {
//const browser = await puppeteer.launch();
const browser = await puppeteer.launch({
ignoreDefaultArgs: ['--disable-extensions'],
});
const page = await browser.newPage();
await page.goto('https://google.com/');
await page.screenshot({path: 'screenshot.png'});
await browser.close();
})();
修正して、実行。
> node index.js
(node:31444) UnhandledPromiseRejectionWarning: Error: Failed to launch the browser process!
TROUBLESHOOTING: https://github.com/puppeteer/puppeteer/blob/master/docs/troubleshooting.md
at onClose (PATH\TO\TEST_PROJECT\puppeteer_test\node_modules\puppeteer\lib\Launcher.js:750:14)
at ChildProcess.<anonymous> (PATH\TO\TEST_PROJECT\puppeteer_test\node_modules\puppeteer\lib\Launcher.js:740:61)
at ChildProcess.emit (events.js:326:22)
at Process.ChildProcess._handle.onexit (internal/child_process.js:276:12)
はい、変わりませんでした。
検証2 (成功)
以上より、 Puppeteer のバージョンをどうにかすれば BackstopJS が動きそうなことが分かりました。
そこで、 Yarn の依存関係の解決オプションの resolutions
を試してみることにします。
// 略
"devDependencies": {
"backstopjs": "^5.0.6", // 追加
"browser-sync": "^2.26.12",
// 略
},
"resolutions": {
"puppeteer": "^5.5.0" // 追加
},
// 略
これで yarn
し直して、念のため backstop_data
ディレクトリ と backstop.json
を削除して backstop init
をやり直し、再度 url
を https://localhost:3000/
に直した上で、 backstop reference
を実行。
> backstop reference
BackstopJS v5.0.6
Loading config: PATH\TO\TEST_PROJECT\randa_back_stopper\backstop.json
COMMAND | Executing core for "reference"
clean | backstop_data/bitmaps_reference was cleaned.
createBitmaps | Selected 1 of 1 scenarios.
CREATING NEW REFERENCE FILE
CREATING NEW REFERENCE FILE
Cookie state restored with: [
{
"path": "/",
"name": "yourCookieName",
"value": "yourCookieValue",
"expirationDate": 1798790400,
"hostOnly": false,
"httpOnly": false,
"secure": false,
"session": false,
"sameSite": "no_restriction",
"url": "https://.www.yourdomain.com"
}
]
Cookie state restored with: [
{
"path": "/",
"name": "yourCookieName",
"value": "yourCookieValue",
"expirationDate": 1798790400,
"hostOnly": false,
"httpOnly": false,
"secure": false,
"session": false,
"sameSite": "no_restriction",
"url": "https://.www.yourdomain.com"
}
]
Browser Console Log 0: JSHandle:BackstopTools have been
installed.
SCENARIO > BackstopJS Homepage
x Close Browser
Browser Console Log 0: JSHandle:BackstopTools have been
installed.
SCENARIO > BackstopJS Homepage
x Close Browser
Run `$ backstop test` to generate diff report.
COMMAND | Command "reference" successfully executed in [3.752s]
動きました!
リグレッションテストの検証
ようやく backstop reference
が動いたので、リグレッションテストを試してみます。
まずは編集前( backstop reference
したとき)のページのキャプチャ画像。
次に、 Scss や JS などいくつかパラメータやコードを編集して、 backstop test
します。
> backstop test
BackstopJS v5.0.6
Loading config: PATH\TO\TEST_PROJECT\randa_back_stopper\backstop.json
COMMAND | Executing core for "test"
createBitmaps | Selected 1 of 1 scenarios.
Cookie state restored with: [
{
"path": "/",
"name": "yourCookieName",
"value": "yourCookieValue",
"expirationDate": 1798790400,
"hostOnly": false,
"httpOnly": false,
"secure": false,
"session": false,
"sameSite": "no_restriction",
"url": "https://.www.yourdomain.com"
}
]
Cookie state restored with: [
{
"path": "/",
"name": "yourCookieName",
"value": "yourCookieValue",
"expirationDate": 1798790400,
"hostOnly": false,
"httpOnly": false,
"secure": false,
"session": false,
"sameSite": "no_restriction",
"url": "https://.www.yourdomain.com"
}
]
Browser Console Log 0: JSHandle:Invisible Full-moon!
Browser Console Log 0: JSHandle:BackstopTools have been
installed.
SCENARIO > BackstopJS Homepage
x Close Browser
Browser Console Log 0: JSHandle:Invisible Full-moon!
Browser Console Log 0: JSHandle:BackstopTools have been
installed.
SCENARIO > BackstopJS Homepage
x Close Browser
COMMAND | Executing core for "report"
See: PATH\TO\TEST_PROJECT\randa_back_stopper\backstop_data\bitmaps_test\yyyymmdd-hhiiss\failed_diff_backstop_default_BackstopJS_Homepage_0_document_0_phone.png
compare | ERROR { requireSameDimensions: true, size: isDifferent, content: 2.06%, threshold: 0.1% }: BackstopJS Homepage backstop_default_BackstopJS_Homepage_0_document_0_phone.png
See: PATH\TO\TEST_PROJECT\randa_back_stopper\backstop_data\bitmaps_test\yyyymmdd-hhiiss\failed_diff_backstop_default_BackstopJS_Homepage_0_document_1_tablet.png
compare | ERROR { requireSameDimensions: true, size: isDifferent, content: 0.70%, threshold: 0.1% }: BackstopJS Homepage backstop_default_BackstopJS_Homepage_0_document_1_tablet.png
report | Test completed...
report | 0 Passed
report | 2 Failed
report | Writing browser report
report | Resources copied
report | Copied json report to: PATH\TO\TEST_PROJECT\randa_back_stopper\backstop_data\bitmaps_test\yyyymmdd-hhiiss\report.json
report | Copied jsonp report to: PATH\TO\TEST_PROJECT\randa_back_stopper\backstop_data\html_report\config.js
COMMAND | Executing core for "openReport"
openReport | Attempting to ping
openReport | Remote not found. Opening backstop_data\html_report\index.html
report | *** Mismatch errors found ***
COMMAND | Command "report" ended with an error after [1.539s]
COMMAND | Error: Mismatch errors found.
at PATH\TO\TEST_PROJECT\randa_back_stopper\node_modules\backstopjs\core\command\report.js:189:17
at processTicksAndRejections (internal/process/task_queues.js:93:5)
COMMAND | Command "test" ended with an error after [5.431s]
COMMAND | Error: Mismatch errors found.
at PATH\TO\TEST_PROJECT\randa_back_stopper\node_modules\backstopjs\core\command\report.js:189:17
at processTicksAndRejections (internal/process/task_queues.js:93:5)
エラーが出ましたね。
また、自動的にブラウザも上がってきて上のように差分も表示されます。
「DIFF」の画像をクリックした先では backstop reference
で撮影したスクリーンショットと backstop test
で撮影したスクリーンショットの差分をワイパーのように比較することができます。
細かく比較できて良いですね。
今度は修正を元に戻して再度 backstop test
。
> backstop test
BackstopJS v5.0.6
Loading config: PATH\TO\TEST_PROJECT\randa_back_stopper\backstop.json
COMMAND | Executing core for "test"
createBitmaps | Selected 1 of 1 scenarios.
Cookie state restored with: [
{
"path": "/",
"name": "yourCookieName",
"value": "yourCookieValue",
"expirationDate": 1798790400,
"hostOnly": false,
"httpOnly": false,
"secure": false,
"session": false,
"sameSite": "no_restriction",
"url": "https://.www.yourdomain.com"
}
]
Cookie state restored with: [
{
"path": "/",
"name": "yourCookieName",
"value": "yourCookieValue",
"expirationDate": 1798790400,
"hostOnly": false,
"httpOnly": false,
"secure": false,
"session": false,
"sameSite": "no_restriction",
"url": "https://.www.yourdomain.com"
}
]
Browser Console Log 0: JSHandle:BackstopTools have been
installed.
SCENARIO > BackstopJS Homepage
Browser Console Log 0: JSHandle:BackstopTools have been
installed.
SCENARIO > BackstopJS Homepage
x Close Browser
x Close Browser
COMMAND | Executing core for "report"
compare | OK: BackstopJS Homepage backstop_default_BackstopJS_Homepage_0_document_0_phone.png
compare | OK: BackstopJS Homepage backstop_default_BackstopJS_Homepage_0_document_1_tablet.png
report | Test completed...
report | 2 Passed
report | 0 Failed
report | Writing browser report
report | Resources copied
report | Copied json report to: PATH\TO\TEST_PROJECT\randa_back_stopper\backstop_data\bitmaps_test\yyyymmdd-hhiiss\report.json
report | Copied jsonp report to: PATH\TO\TEST_PROJECT\randa_back_stopper\backstop_data\html_report\config.js
COMMAND | Executing core for "openReport"
openReport | Attempting to ping
openReport | Remote not found. Opening backstop_data\html_report\index.html
COMMAND | Command "report" successfully executed in [0.698s]
COMMAND | Command "test" successfully executed in
[4.135s]
成功です。
今度はテスト成功の画面で、同じスクリーンショットが撮影できていることが確認できます。
ということで、軽くですが BackstopJS が使えることを確認しました。
実際にはシナリオの編集とか色々あるようですが、ひとまず動作確認が取れたということで。
追加検証
動作確認が取れたところで、より実践的になりそうな部分を深堀り。
- ブラウザサイズ: PCサイズも追加
- BrowserSync のリロード通知を削除: スクリーンショットのタイミングである/なしで分かれてしまうので一律非表示にしてDIFF検知されないようにする
- 複数ページの場合:
- シナリオを追加
label
をファイル名に使用するので半角英数字で記述、ページごとにユニークな名前にする
- 縦長ページの場合: 普通に全体のスクリーンショットを撮影する
position: fixed;
のナビゲーションバー: 上端
何回かテストを試して気になったのはこの辺りですかね。いずれもクリア、問題なさそうです。
わざと失敗させた場合の画面。
縦長ページでは普通に下までスクロールできます。
成功画面。最初のテストのときと同様、特に問題はなさそうです。
リポジトリ
作成したリポジトリを貼っておきます。
参考
BackstopJS
- backstopjs – npm
- garris/BackstopJS: Catch CSS curve balls.
- BackstopJSでビジュアルリグレッションテスト! – Qiita
- BackstopJSで始めるリグレッションテスト – Qiita
- BackstopJS を利用した Visual Regression Testing の感想と工夫ポイント – upinetree’s memo
- Backstop.jsでクリスマスを少し幸せにするリファクタリング – Qiita
- BackstopJSで行うビジュアルリグレッションテスト ++ Gaji-Laboブログ
Puppeteer の Error: Failed to launch the browser process! エラー対処
- puppeteer/troubleshooting.md at main · puppeteer/puppeteer
- Node.jsとPuppeteerでChromeを自動操縦する | 🌴 officeの杜 🥥
- Error: Failed to launch the browser process! spawn /app/node_modules/puppeteer/.local-chromium/linux-756035/chrome-linux/chrome ENOENT · Issue #5992 · puppeteer/puppeteer