tag:crieit.net,2005:https://crieit.net/tags/%E3%83%93%E3%82%B8%E3%83%A5%E3%82%A2%E3%83%AB%E3%83%AA%E3%82%B0%E3%83%AC%E3%83%83%E3%82%B7%E3%83%A7%E3%83%B3%E3%83%86%E3%82%B9%E3%83%88/feed 「ビジュアルリグレッションテスト」の記事 - Crieit Crieitでタグ「ビジュアルリグレッションテスト」に投稿された最近の記事 2021-07-31T17:37:19+09:00 https://crieit.net/tags/%E3%83%93%E3%82%B8%E3%83%A5%E3%82%A2%E3%83%AB%E3%83%AA%E3%82%B0%E3%83%AC%E3%83%83%E3%82%B7%E3%83%A7%E3%83%B3%E3%83%86%E3%82%B9%E3%83%88/feed tag:crieit.net,2005:PublicArticle/17548 2021-07-31T17:37:19+09:00 2021-07-31T17:37:19+09:00 https://crieit.net/posts/cypres-bij-sterrennacht-vrtest-20210731 Cypress + reg-cli でビジュアルリグレッションテスト環境を作る <h2 id="経緯"><a href="#%E7%B5%8C%E7%B7%AF">経緯</a></h2> <ul> <li><a href="https://crieit.net/posts/backstopjs-test-20201122">BackstopJS を試す (Error: Failed to launch the browser process! エラー発生→ puppeteer のバージョンを指定して解決) - Crieit</a></li> <li><a href="https://crieit.net/posts/backstop-trial-and-error-20201215">BackstopJS で背景画像の高さを `vh` 単位で指定したページで画像やスクリーンショットが引き伸ばされる現象についてメモ (未解決) - Crieit</a> <ul> <li><a target="_blank" rel="nofollow noopener" href="https://github.com/arm-band/test_backstop_randa_back_stopper_2">arm-band/test_backstop_randa_back_stopper_2: BackstopJS のテストです。</a></li> </ul></li> </ul> <p>BackstopJS が上手く行かなかったので Cypress を使う方法に切り替えたところ、わりと上手く行きそうだったのでこちらの方法に切り替えてみました。</p> <h2 id="成果物"><a href="#%E6%88%90%E6%9E%9C%E7%89%A9">成果物</a></h2> <ul> <li><a target="_blank" rel="nofollow noopener" href="https://github.com/arm-band/cypres_bij_sterrennacht">GitHub - arm-band/cypres_bij_sterrennacht: Cypress + reg-cli によるビジュアルリグレッションテストのスクリプト。</a></li> </ul> <p>スクリプトのベースは</p> <ul> <li><a target="_blank" rel="nofollow noopener" href="https://qiita.com/yodyman/items/3a2fbb106eaaca3aca47">Cypress + REG CLI ではじめたビジュアルリグレッションテスト - Qiita</a></li> </ul> <p>こちらを参考に、自分で使いやすいようにさらにいくつかチョイ足しをした感じになります。</p> <h2 id="構成"><a href="#%E6%A7%8B%E6%88%90">構成</a></h2> <h3 id="ディレクトリ構造"><a href="#%E3%83%87%E3%82%A3%E3%83%AC%E3%82%AF%E3%83%88%E3%83%AA%E6%A7%8B%E9%80%A0">ディレクトリ構造</a></h3> <pre><code>PROJECT_ROOT/ ├ cypress/ │ ├ fixtures/ │ │ └ visual-regression-test/ │ │ └ config.json.sample // 設定サンプル │ │ │ ├ integration/ │ │ └ visual-regression-test/ │ │ ├ cafe_bij_nacht-before.spec.js // ref のキャプチャを取得するスクリプト │ │ └ cafeterras_bij_nacht-after.spec.js // test のキャプチャを取得するスクリプト │ │ │ ├ plugins/ │ │ └ index.js // イベントにフックさせて動作させるスクリプト │ │ │ └ support/ // 略 │ ├ package.json ├ sterrennacht.js // Cypress + reg-cli のチェック後に reg-cli のレポートのHTMLを開く └ zonnebloemen.js // Cypress 実行時の設定パラメータを調整する </code></pre> <h3 id="package.json"><a href="#package.json">package.json</a></h3> <pre><code class="json">{ "name": "cypres_bij_sterrennacht", "version": "0.0.1", "description": "Cypress + reg-cli によるビジュアルリグレッションテストのスクリプト。", "main": "sterrennacht.js", "scripts": { "result": "node ./sterrennacht", "cypress:before": "cypress run --config-file false --config trashAssetsBeforeRuns=true -s cypress/integration/visual-regression-test/cafe_bij_nacht-before.spec.js", "cypress:after": "cypress run --config-file false --config trashAssetsBeforeRuns=false -s cypress/integration/visual-regression-test/cafeterras_bij_nacht-after.spec.js", "cypress": "run-s cypress:*", "reg": "reg-cli ./cypress/screenshots/visual-regression-test/before ./cypress/screenshots/visual-regression-test/after ./cypress/screenshots/visual-regression-test/diff -R ./cypress/screenshots/visual-regression-test/report/index.html -M 0.2 -I", "test": "run-s cypress:* reg result" }, "author": "Arm Band", "license": "ISC", "dependencies": { "browser-sync": "^2.26.14", "cypress": "^7.6.0", "npm-run-all": "^4.1.5", "opener": "^1.5.2", "reg-cli": "^0.17.0" } } </code></pre> <p>npm scripts にタスクを記述、 <code>yarn test</code> で以下の4つのタスクを一括で動かします。</p> <ol> <li>Cypress による変更<strong>前</strong>サイトのキャプチャの撮影</li> <li>Cypress による変更<strong>後</strong>サイトのキャプチャの撮影</li> <li><code>reg-cli</code> で1., 2.の比較とレポート生成</li> <li>レポートのHTMLを <code>openner</code> で開く</li> </ol> <h3 id="zonnebloemen.js"><a href="#zonnebloemen.js">zonnebloemen.js</a></h3> <pre><code class="javascript">/** * zonnebloemen : 設定を生成する */ class zonnebloemen { /** * getParameters : 設定用のフラグを生成 * * @param {booelan} flag : フラグ * * @return {Object} : 設定パラメータの内容 */ getParameters (flag) { return flag === 'BEFORE' ? { outStr: 'before', urlKey: 'ref', flag : true, } : { outStr: 'after', urlKey: 'test', flag : false, }; }; /** * getConfig : 設定を生成 * * @param {Object} data : JSONファイル のデータ * @param {booelan} flag : フラグ * * @return {Object} : 設定の内容 */ getConfig (data, flag) { if (data !== undefined && data !== null && Object.keys(data).length > 0) { flag ? data.commons['trashAssetsBeforeRuns'] = true : data.commons['trashAssetsBeforeRuns'] = false; } return data; }; }; module.exports = zonnebloemen; </code></pre> <p>フラグによって変更前なのか変更後なのか、スクリプトを実行する際の設定の配列のパラメータを弄るクラス。</p> <p>具体的には以下の2つ。</p> <ul> <li>変更前 <ul> <li>参照先URL: <code>ref</code>キー にセットされた値</li> <li><code>trashAssetsBeforeRuns</code>(キャプチャ取得スクリプト実行前にキャプチャ格納ディレクトリをクリーンアップするか): <code>true</code></li> </ul></li> <li>変更後 <ul> <li>参照先URL: <code>test</code>キー にセットされた値</li> <li><code>trashAssetsBeforeRuns</code>: <code>false</code></li> </ul></li> </ul> <h3 id="config.json.sample"><a href="#config.json.sample">config.json.sample</a></h3> <pre><code class="json">{ "commons": { "url": { "ref": "https://www.example.jp", "test": "https://demo.example.jp" }, "hideElements": [ "#__bs_notify__", ".returnPageTop", ".c-returnPageTop", "#navbar", ".navbar" ], "screenshot": { "capture": "fullPage", "waitMsec": 1000 } }, "viewports": [ { "name": "phone", "width": 375, "height": 667 }, { "name": "tablet", "width": 1024, "height": 768 }, { "name": "pc", "width": 1920, "height": 1080 } ], "pages": [ { "test_id": "top", "uri": "/" }, { "test_id": "about", "uri": "/about.html" } ] } </code></pre> <p>サンプルですがやりたいことは最低限押さえてあるかと。</p> <ul> <li><code>commons</code>: <ul> <li><code>url</code>: <ul> <li><code>ref</code>: お手本として参照するサイトのURL</li> <li><code>test</code>: テストしたいサイトのURL</li> </ul></li> </ul></li> <li><code>viewports</code>: デバイス(ブラウザ)の幅と高さ。デフォルトではスマートフォン、タブレット、PCとして3つのサイズを指定</li> <li><code>pages</code>: テストしたいページのURLとキャプチャのファイル名に使用するID</li> </ul> <p>基本的にこの設定に従って Cypress でキャプチャを撮っていきます。</p> <h3 id="cafe_bij_nacht-before.spec.js"><a href="#cafe_bij_nacht-before.spec.js">cafe_bij_nacht-before.spec.js</a></h3> <pre><code class="javascript">const flag = 'BEFORE'; const configDataOrigin = require('../../fixtures/visual-regression-test/config.json'); const zonnebloemenClass = require('../../../zonnebloemen'); const zonnebloemen = new zonnebloemenClass(); const parameters = zonnebloemen.getParameters(flag); const configData = zonnebloemen.getConfig(configDataOrigin, parameters.flag); describe(`${parameters.outStr} screenshot`, function () { const pages = configData.pages; const domain = configData.commons.url[parameters.urlKey]; const viewports = configData.viewports; const ss_dir = parameters.outStr; var test_id = ''; var ss_path = ''; var url = ''; pages.forEach(({ test_id, uri }) => { viewports.forEach(({ name, width, height }) => { context(test_id, () => { beforeEach(() => { url = domain + uri; cy.visit(url); }); it('take screenshot', function () { ss_path = ss_dir + '/' + test_id + '-' + name; cy.wait(configData.commons.screenshot.waitMsec); cy.viewport(width, height); cy.scrollTo('bottom'); cy.screenshot(ss_path, { onBeforeScreenshot($el) { for (const selector of configData.commons.hideElements) { const $selector = $el.find(selector); if ($selector) { $selector.hide(); } } }, capture: Cypress.env(configData.commons.screenshot.capture), }); }); }); }); }); }); </code></pre> <p>最初の定数 <code>flag</code> 以外は <code>cafe_bij_nacht-before.spec.js</code> と <code>cafeterras_bij_nacht-after.spec.js</code> で共通となります。本当は1つのファイルにしたかったのですが上手く動かなかったので2つにしました。</p> <p>やっていることは <code>flag</code> によって <code>zonnebloemen.js</code> で設定パラメータの配列を生成、その値を以てキャプチャを撮影する、というもの。</p> <h3 id="cafeterras_bij_nacht-after.spec.js"><a href="#cafeterras_bij_nacht-after.spec.js">cafeterras_bij_nacht-after.spec.js</a></h3> <pre><code class="javascript">const flag = 'AFTER'; const configDataOrigin = require('../../fixtures/visual-regression-test/config.json'); const zonnebloemenClass = require('../../../zonnebloemen'); const zonnebloemen = new zonnebloemenClass(); const parameters = zonnebloemen.getParameters(flag); const configData = zonnebloemen.getConfig(configDataOrigin, parameters.flag); describe(`${parameters.outStr} screenshot`, function () { const pages = configData.pages; const domain = configData.commons.url[parameters.urlKey]; const viewports = configData.viewports; const ss_dir = parameters.outStr; var test_id = ''; var ss_path = ''; var url = ''; pages.forEach(({ test_id, uri }) => { viewports.forEach(({ name, width, height }) => { context(test_id, () => { beforeEach(() => { url = domain + uri; cy.visit(url); }); it('take screenshot', function () { ss_path = ss_dir + '/' + test_id + '-' + name; cy.wait(configData.commons.screenshot.waitMsec); cy.viewport(width, height); cy.scrollTo('bottom'); cy.screenshot(ss_path, { onBeforeScreenshot($el) { for (const selector of configData.commons.hideElements) { const $selector = $el.find(selector); if ($selector) { $selector.hide(); } } }, capture: Cypress.env(configData.commons.screenshot.capture), }); }); }); }); }); }); </code></pre> <p>上述で説明した通りなので省略。</p> <h3 id="plugins/index.js"><a href="#plugins%2Findex.js">plugins/index.js</a></h3> <pre><code class="javascript">/// <reference types="cypress" /> // *********************************************************** // This example plugins/index.js can be used to load plugins // // You can change the location of this file or turn off loading // the plugins file with the 'pluginsFile' configuration option. // // You can read more here: // https://on.cypress.io/plugins-guide // *********************************************************** // This function is called when a project is opened or re-opened (e.g. due to // the project's config changing) /** * @type {Cypress.PluginConfig} */ // eslint-disable-next-line no-unused-vars const path = require('path'); const fs = require('fs'); const isExistFile = (file) => { try { fs.statSync(file); return true; } catch (err) { if (err.code === 'ENOENT') { return false; } } }; module.exports = (on, config) => { on('after:screenshot', (details) => { const str = details.path; const newPath = str.replace(/[\w\d_]+\-(before|after)\.spec\.js/, ''); const newDir = path.dirname(newPath); fs.mkdirSync(newDir, { recursive: true }); fs.renameSync(details.path, newPath); return { path: newPath }; }); on('after:spec', (details) => { const newPath = details.absolute.replace('integration', 'screenshots'); newPath.match(/[\w\d_]+\-(before|after)\.spec\.js/); const childNewPath = path.join(newPath, RegExp.$1); if (isExistFile(childNewPath)) { fs.rmdirSync(childNewPath); } if (isExistFile(newPath)) { fs.rmdirSync(newPath); } }); }; </code></pre> <p>通常だとキャプチャの保存先のディレクトリ名がキャプチャ撮影スクリプトのファイル名そのものになってしまい、ディレクトリ名にドットが入るなどややもやっとする形式なので、そこを <code>before</code> or <code>after</code> のみになるように保存先ディレクトリのリネーム (ファイルコピー+オリジナルディレクトリ削除) を実行するようにしています。</p> <h3 id="sterrennacht.js"><a href="#sterrennacht.js">sterrennacht.js</a></h3> <pre><code class="javascript">const opener = require('opener'); const dir = { baseDir: './cypress/screenshots/visual-regression-test/', startPath: 'report/index.html', }; opener(`${dir.baseDir}${dir.startPath}`); </code></pre> <p><code>opener</code> で開くだけの簡単なスクリプト。</p> <h2 id="実行結果(サンプル)"><a href="#%E5%AE%9F%E8%A1%8C%E7%B5%90%E6%9E%9C%28%E3%82%B5%E3%83%B3%E3%83%97%E3%83%AB%29">実行結果(サンプル)</a></h2> <p>試しに少し動かしてみます。サンプルは<a target="_blank" rel="nofollow noopener" href="https://ususama.ewigleere.net/">ホーム | Kiribi Ususama</a>で、公開されているものを <code>ref</code> 、ローカルで Browsersync で開いたものを <code>test</code> として動作させてみます。</p> <p><a href="https://crieit.now.sh/upload_images/b8395bd7e1285b90b36516b0f4eaf57161050ba36716a.jpg" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/b8395bd7e1285b90b36516b0f4eaf57161050ba36716a.jpg?mw=700" alt="reg-cli レポートのトップ" /></a></p> <p><code>reg-cli</code> のレポートのトップ。キャプチャのうちテストを通らなかったものが上部に、通ったものは下部に分類されて並べられています。</p> <p><a href="https://crieit.now.sh/upload_images/70a0a871c47de51ae4439ec6a769c4fa61050bacf3c2e.jpg" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/70a0a871c47de51ae4439ec6a769c4fa61050bacf3c2e.jpg?mw=700" alt="テストOKの例" /></a></p> <p>テストが通った例。トップページです。</p> <p><a href="https://crieit.now.sh/upload_images/d2b7e96fc061ad990c86cbdb10dcdbd961050bb5236f1.jpg" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/d2b7e96fc061ad990c86cbdb10dcdbd961050bb5236f1.jpg?mw=700" alt="テストNGの例" /></a></p> <p>テストNGの例。なんだかディレクトリ構造のツリー構造がずれているようです。左右にバーをスライドさせて比較を行うことができる他、差分を赤色で表示させるモードや、 <code>ref</code> のみの表示、 <code>test</code> のみの表示、と切り替えることもできます。ここも使いやすそうだとおもった点です。</p> <p>これで、自分なりにビジュアルリグレッションテストができる環境が整備できた気がします。</p> <h2 id="余談"><a href="#%E4%BD%99%E8%AB%87">余談</a></h2> <p>もはや恒例となりましたが、名前について。今回は「Cypres bij Sterrennacht」。オランダ語です。日本語訳すると「糸杉と星の見える道」。ゴッホの最晩年の作品ですね。名前は Cypress をツールとして使用しているから、というシンプルな理由です。</p> <p>ちなみに自前の補助スクリプトの名前も</p> <ul> <li><code>cafe_bij_nacht-before.spec.js</code>: Cafe bij nacht →「夜のカフェ」</li> <li><code>cafeterras_bij_nacht-after.spec.js</code>: Cafeterras bij nacht →「夜のカフェテラス」</li> <li><code>sterrennacht.js</code>: Sterrennacht →「星月夜」</li> <li><code>zonnebloemen.js</code>: Zonnebloemen →「ひまわり」</li> </ul> <p>でいずれもゴッホの作品の名前から採りました。</p> <h2 id="参考"><a href="#%E5%8F%82%E8%80%83">参考</a></h2> <h3 id="Cypress"><a href="#Cypress">Cypress</a></h3> <h4 id="きっかけ"><a href="#%E3%81%8D%E3%81%A3%E3%81%8B%E3%81%91">きっかけ</a></h4> <ul> <li><a target="_blank" rel="nofollow noopener" href="https://future-architect.github.io/articles/20210428a/">Cypress入門~初心者でも簡単にE2Eテストが作れる~ | フューチャー技術ブログ</a></li> <li><a target="_blank" rel="nofollow noopener" href="https://future-architect.github.io/articles/20210428d/">Cypress - 書きやすいテストの秘密と独自コマンドの実装 | フューチャー技術ブログ</a></li> <li><a target="_blank" rel="nofollow noopener" href="https://qiita.com/okitan/items/b44882e28006c1be32b7">cypressを触ってみた - Qiita</a></li> </ul> <h4 id="ベース"><a href="#%E3%83%99%E3%83%BC%E3%82%B9">ベース</a></h4> <ul> <li><a target="_blank" rel="nofollow noopener" href="https://qiita.com/yodyman/items/3a2fbb106eaaca3aca47">Cypress + REG CLI ではじめたビジュアルリグレッションテスト - Qiita</a></li> </ul> <h4 id="Docs"><a href="#Docs">Docs</a></h4> <ul> <li><a target="_blank" rel="nofollow noopener" href="https://docs.cypress.io/guides/overview/why-cypress">Why Cypress? | Cypress Documentation</a></li> </ul> <h4 id="前回実施時のスクリーンショットの破棄 (trashAssetsBeforeRuns)"><a href="#%E5%89%8D%E5%9B%9E%E5%AE%9F%E6%96%BD%E6%99%82%E3%81%AE%E3%82%B9%E3%82%AF%E3%83%AA%E3%83%BC%E3%83%B3%E3%82%B7%E3%83%A7%E3%83%83%E3%83%88%E3%81%AE%E7%A0%B4%E6%A3%84+%28trashAssetsBeforeRuns%29">前回実施時のスクリーンショットの破棄 (trashAssetsBeforeRuns)</a></h4> <ul> <li><a target="_blank" rel="nofollow noopener" href="https://stackoverflow.com/questions/55098694/how-to-delete-all-screenshots-before-a-test-run-while-doing-a-cypress-visual-reg">How to delete all screenshots before a test run while doing a cypress visual regression - Stack Overflow</a></li> <li><a target="_blank" rel="nofollow noopener" href="https://uga-box.hatenablog.com/entry/2020/08/04/000000">【Cypress】Screenshotの設定 - UGA Boxxx</a></li> </ul> <h4 id="スクロール"><a href="#%E3%82%B9%E3%82%AF%E3%83%AD%E3%83%BC%E3%83%AB">スクロール</a></h4> <ul> <li><a target="_blank" rel="nofollow noopener" href="https://docs.cypress.io/api/commands/scrollto#Syntax">scrollTo | Cypress Documentation</a></li> </ul> <h4 id="要素を隠す"><a href="#%E8%A6%81%E7%B4%A0%E3%82%92%E9%9A%A0%E3%81%99">要素を隠す</a></h4> <ul> <li><a target="_blank" rel="nofollow noopener" href="https://docs.cypress.io/api/cypress-api/screenshot-api#Change-the-DOM-using-onBeforeScreenshot-and-onAfterScreenshot">Cypress.Screenshot | Cypress Documentation</a></li> </ul> <p><code>.hide()</code></p> <h4 id="find の戻り値"><a href="#find+%E3%81%AE%E6%88%BB%E3%82%8A%E5%80%A4">find の戻り値</a></h4> <ul> <li><a target="_blank" rel="nofollow noopener" href="https://docs.cypress.io/api/commands/find#Command-Log">find | Cypress Documentation</a></li> </ul> <h4 id="pluginsスクリプト のイベント"><a href="#plugins%E3%82%B9%E3%82%AF%E3%83%AA%E3%83%97%E3%83%88+%E3%81%AE%E3%82%A4%E3%83%99%E3%83%B3%E3%83%88">pluginsスクリプト のイベント</a></h4> <ul> <li><a target="_blank" rel="nofollow noopener" href="https://docs.cypress.io/guides/tooling/plugins-guide#Run-Lifecycle">Plugins | Cypress Documentation</a></li> </ul> <h4 id="config"><a href="#config">config</a></h4> <ul> <li><a target="_blank" rel="nofollow noopener" href="https://docs.cypress.io/guides/references/configuration#Command-Line">Configuration | Cypress Documentation</a></li> </ul> <h4 id="未使用"><a href="#%E6%9C%AA%E4%BD%BF%E7%94%A8">未使用</a></h4> <ul> <li><a target="_blank" rel="nofollow noopener" href="https://qiita.com/motodimago/items/9fb010026cb14ccab2b7">Cypressのheadlessモードでlogを表示させる - Qiita</a></li> <li><a target="_blank" rel="nofollow noopener" href="https://github.com/cypress-io/cypress/issues/186">Ability to add a message to stdout during cypress run ・ Issue #186 ・ cypress-io/cypress ・ GitHub</a></li> <li><a target="_blank" rel="nofollow noopener" href="https://stackoverflow.com/questions/52070262/cypress-pipe-console-log-and-command-log-to-output">e2e testing - Cypress pipe console.log and command log to output - Stack Overflow</a></li> <li><a target="_blank" rel="nofollow noopener" href="https://docs.cypress.io/api/cypress-api/config#Notes">Cypress.config | Cypress Documentation</a> <ul> <li><a target="_blank" rel="nofollow noopener" href="https://qiita.com/jnuank/items/25a1664ad186c9fb34e8">Vuetifyで作った画面のテストをCypressで自動化しようとした時に調べたことや、つまづいたこと - Qiita</a></li> </ul></li> </ul> <h3 id="reg-cli"><a href="#reg-cli">reg-cli</a></h3> <ul> <li><a target="_blank" rel="nofollow noopener" href="https://github.com/reg-viz/reg-cli">reg-viz/reg-cli: ?? Visual regression test tool.</a></li> </ul> arm-band