Counts the Clouds

Cypressとreg-cliで画像回帰テスト
2022.05.06

Next.js
Cypress
reg-cli

kristof-van-rentergem-wpJzHTg2UA8-unsplash.jpg

Cypressreg-cliを使って、手元で画像回帰テストをしてみた。姉妹品でreg-suitというS3などの外部と連携できるツールもあるが、今回は簡単にしたかったので薄いreg-cliを選択。

Next.jsのサンプルを構成

検証のため複数ページを簡単に構成する必要があったのでNext.jsのダイナミックルーティングのサンプルを使うことにした。

% npx create-next-app --example dynamic-routing dynamic-routing-app
# ...
% cd dynamic-routing-app
% yarn dev

これでダイナミックルーティングのサンプルアプリが開始されることを確認。

Cypressの導入

Cypressを追加する。まずは、公式のチュートリアルを動かせるようにする。

% yarn add -D cypress
% yarn run cypress open

これでCypressのテストランナーが動く。package.jsonにコマンドとして追加した。

  "test": "cypress open"
% yarn test # cypress open

cypress/integration以下にある例を削除して、新しくsample_spec.jsを追加。

% rm -rf cypress/integration/{1-getting-started,2-advanced-examples}
% touch cypress/integration/sample_spec.js

次のテストを追加。

// sample_spec.js
describe('My First Test', () => {
  it('Visits the Kitchen Sink', () => {
    cy.visit('https://example.cypress.io')
  })
})
% yarn test

テストランナーが開くので、sample_spec.jsのリンクをクリックするとテストが動く。

このテストランナーは、画面上の要素をセレクター形式で取得できたりして、テストの構築自体にも便利。

Cypressでスクリーンショットを撮る

別プロセスでNext.jsのアプリを立ち上げておく。

% yarn build
% yarn start

スクリーンショットを撮るように、テストを変更。

// sample_spec.js
Cypress.Screenshot.defaults({ overwrite: true })

describe('My First Test', () => {
  it('Visits the Kitchen Sink', () => {
    cy.visit('http://localhost:3000').screenshot('Home')
  })
})

テストランナーを使わずに即実行するようにpackage.jsonを変更。

  "test": "cypress run",
% yarn test # cypress run
# ...
  (Screenshots)

  -  /Users/same/dynamic-routing-app/cypress/screenshots/sample_spec.js/Home.png          (1000x660)
# ...

これでスクリーンショットが撮れるようになった。cypress/screenshotsに保存されている。

テストと設定をまとめる

cypress.jsonに設定を少し追加しておく。

// cypress.json
{
  "baseUrl": "http://localhost:3000",
  "trashAssetsBeforeRuns": true,
  "video": false
}

サンプルアプリのその他のルーティングを追加して、最終的にテストは以下のようになっている。

// sample_spec.js
Cypress.Screenshot.defaults({ overwrite: true })

describe('My First Test', () => {
  it('Run through and take screenshots', () => {
    cy.visit('/').screenshot('Home')
    cy.visit('/about').screenshot('About')
    cy.visit('/post/first').screenshot('First')
    cy.visit('/post/second').screenshot('Second')
  })
})

Cypressにクリックをさせて画面遷移してもよいが、動的なナビゲーション(例えばレスポンシブで隠れてしまうなど)の場合に少し面倒になる。この場合は単にスクリーンショットを撮りたいだけなので、すべてvisitで直接開くようにした。

reg-cliの導入

% yarn add -D reg-cli

reg-cliのコマンドを見てみると、expect(旧画像)とactual(新画像)を比較してdiffのディレクトリに差分を出力するという形になっている。これに合わせてディレクトリとコマンドを用意する。

$ reg-cli /path/to/actual-dir /path/to/expected-dir /path/to/diff-dir -R ./report.html

ディレクトリ構成

今回は以下の通りディレクトリを生成するようにした。

.
└── .reg
    ├── actual
    ├── diff
    └── expect

実は、これはreg-suitの構成を拝借している。

スクリーンショットの処理とレポートの実行

今回は、手元でコマンドを実行して差分が期待どおりか、想定外の変更がでていないか人間が見る用途を想定した。回帰テストと言っているものの、ここでは差分が発生する前提で使っている。そういう意味では差分の承認としてexpectをactualで上書きする処理も必要だったかも。

  1. 旧画像を生成
  2. 新画像を生成
  3. 差分を生成、レポートを表示

例えば、旧画像としてmasterをチェックアウトしてスクリーンショットを撮影し、新画像としてfeatureブランチをチェックアウトしてスクリーンショットを撮影、差分を見るといった手順になる。package.jsonにコマンドを追加。

    "test:expect": "rm -rf .reg && mkdir -p .reg/{actual,diff,expect} && cp -r cypress/screenshots/* .reg/expect",
    "test:actual": "mkdir -p .reg/{actual,diff,expect} && cp -r cypress/screenshots/* .reg/actual",
    "test:diff": "reg-cli .reg/actual .reg/expect .reg/diff -J .reg/reg.json -R .reg/index.html -M 0.1; npx http-server -c-1 .reg"

画像回帰テストの実行

旧画像の生成

さっそく、test:expectを実行。

% yarn test:expect

先ほどスクリーンショットは撮影済みなので、これで旧画像としてmasterブランチのスクリーンショットがコピーされた。ここまでで、作業をコミットしておく。

% git init

Cypressなどのアセットを無視するように.gitingoreに追加。

# cypress
/cypress/screenshots
/cypress/videos
.reg
% git add .
% git commit -m "first commit"

新画像の生成

差分を出すため、少し変更を加える。

diff --git a/pages/about.js b/pages/about.js
index 255d076..4d1ace3 100644
--- a/pages/about.js
+++ b/pages/about.js
@@ -3,7 +3,7 @@ import Header from '../components/header'
 const About = () => (
   <>
     <Header />
-    <h1>About page</h1>
+    <h1>About page!!!</h1>
   </>
 )

diff --git a/pages/index.js b/pages/index.js
index 0f56525..4786d42 100644
--- a/pages/index.js
+++ b/pages/index.js
@@ -3,7 +3,7 @@ import Header from '../components/header'
 const Home = () => (
   <>
     <Header />
-    <h1>Hello World!</h1>
+    <h1>Hello World!!!</h1>
   </>
 )

diff --git a/pages/post/[id]/index.js b/pages/post/[id]/index.js
index c8f4a77..092545c 100644
--- a/pages/post/[id]/index.js
+++ b/pages/post/[id]/index.js
@@ -9,16 +9,16 @@ const Post = () => {
   return (
     <>
       <Header />
-      <h1>Post: {id}</h1>
+      <h1>Super Post: {id}</h1>
       <ul>
         <li>
           <Link href="/post/[id]/[comment]" as={`/post/${id}/first-comment`}>
-            <a>First comment</a>
+            <a>First comment!</a>
           </Link>
         </li>
         <li>
           <Link href="/post/[id]/[comment]" as={`/post/${id}/second-comment`}>
-            <a>Second comment</a>
+            <a>Second comment!!</a>
           </Link>
         </li>
       </ul>

別プロセスでサーバーを開始しておく。

% yarn build
% yarn start

featureブランチのつもりでブランチを切って変更をコミット。

% git checkout -b changed
% git add .
% git commit -m "changed"

変更後のスクリーンショットの撮影、新画像へコピーを行う。

% yarn test # cypress run
% yarn test:actual

検証

% yarn test:diff

検証ツールが立ち上がるので、差分を確認。

今回はCypressを使用したが、ほかのE2Eテストを行わないのであればpuppeteerでも十分かもしれない。