BackstopJSをMagento2のUIレグレッションテストに導入してみた。

本記事は[Magento2 Advent Calendar 2018](https://adventar.org/calendars/3176) 16日目の記事になります。
前回の記事 : [Magento2をDockerで動かすには？【docker-compose編】](https://principle-works.jp/blog/how2runm2ondocker-compose/)
#はじめに

日々の業務では、Magento2のスタイル調整などを行っていたりします。

Magento2のスタイル調整では、Magentoの特徴とも言える「継承・上書き」を利用しつつ、各モジュールに散らばるLESSファイルを修整していくことになります。

詳しいスタイル変更の仕組みはこちら記事が解説して下さっております。

[Magento2のスタイル（LESS）変更の方法概要](https://qiita.com/kzkiq2nd/items/4a4adaf5544ad5f0d840)

複数のページにまたがるUIパーツをLESS変数を利用して調整していくわけですが、LESSを利用したスタイル調整では、LESS変数を用いて効率的にスタイル調整を行える一方で、予期せぬページで思わぬデザイン崩れを起こしてしまうことが多々あります。

Magento2のフロントエンド開発を効率良く進めていくためには、できるだけ手作業でのチェックと自分の変更を反映する際の不安を最小限にすることが必要だと思い、UIテストの自動ツールを探していたところ、非常に興味深いツールBackstopJSというUIレグレッションツールを発見いたしました。

今回の記事では、BackstopJSをMagento2で利用するためのカスタマイズ方法を紹介したいと思います。

## 目次
1. [BackstopJSとは](#1-backstopjsとは)
2. [BackstopJsのインストール・導入](#2-backstopjsのインストール導入)
3. [設定ファイルのカスタマイズ(for Magento2)](#3-設定ファイルのカスタマイズfor-magento2)
4. [画像ファイルの差し替え方法(for Magento2)](#4-画像ファイルの差し替え方法for-magento2)
5. [Magento2用のシナリオ作成](#5-magento2用のシナリオ作成)


# 1. <span id="hh1">BackstopJSとは</span>

[BackstopJS](https://github.com/garris/BackstopJS)とは、ページのスクリーンショットを比較して、自動でUIの変更を比較出来る大変便利なツールです。

<img width="499" alt="スクリーンショット 2018-12-16 18.28.07.png" src="https://qiita-image-store.s3.amazonaws.com/0/97920/8a2cffef-6750-5c3e-5155-4bda7f212a82.png">


オプションの数が少なく大変使いやすい上に、拡張性もありとても強力なUIテストツールだと感じました。

PhantomCSSなど、その他のUIレグレッションツールも試してみましたが、現状BackstopJSが一番使いやすいのでは。

# 2. BackstopJSのインストール・導入

###BackstopJSのインストール

BackstopJsはnpmのパッケージとして配布されています。

```npm install -g backstopjs```

レンダリング用のブラウザとして、Headless Chromeがバンドルされてきます。

BackstopJSではレンダリングブラウザとして、Headless Chromeの他に、PhantomJs、SlimerJsをサポートしているので、好みに応じて使い分けることが出来ます。 (PhantomJs、SlimerJsは別途インストールが必要。)


UIテストに必要なファイルは全て一括でインストールされるため、SeleniumのようにWebDriverのバージョン違いで上手く動かないといったような心配は必要なさそうです。


###BackstopJSの初期化

```backstop init```

↑初期化コマンドを叩くと、設定ファイルとデータ保存用のディレクトリが作成されます。
> backstop/
> 　├ backstop.json --設定ファイル
> 　├ backstop_data/ --スクリーンショットの保存先 & シナリオ作成(後述)


###BackstopJSのデモンストレーション

生成されたbackstop.json(設定ファイル)はすでに、デモ用の設定がされているので、早速動かしてみます。

```backstop reference ```
・ リファレンス用のスクリーンショットを取得
・ backstop_data/配下に画像を保存

```backstop test ```
・テスト用のスクリーンショットを取得
・backsto_data/配下に画像を保存
・リファレンスとして保存した画像と比較し、変更内容をブラウザに表示

backstop testコマンドを入力すると、テスト用のスクリーンショットを取得した後、リファレンス用のスクリーンショットと比較し、結果をレポートとしてブラウザに表示してくれます。
下記のような画像が表示されていれば成功です。
デモでは同じサイトを比較しているので、<font color="YellowGreen">オールグリーン</font>の状態になっています。

![スクリーンショット 2018-12-11 22.19.40.png](https://qiita-image-store.s3.amazonaws.com/0/97920/2a1f04b8-cb20-cf01-5b3a-275ba72bb948.png)

例えば、さきほどのページの下部にあるGithub画像を差し替えてみると、表示が異なる部分がしっかりと<font color="DeepPink">ピンク色</font>で表示されていることが分かります。

![スクリーンショット 2018-12-11 22.34.16.png](https://qiita-image-store.s3.amazonaws.com/0/97920/fc82956c-1be0-b9aa-a9e2-ba8237c5dbcb.png)

非常に分かりやすい。:frowning2:


# 3. 設定ファイルのカスタマイズ(for Magento2)

BackstopJSのカスタマイズは基本的にはbackstop initで作成されたbackstop.jsonファイルを編集していくことになりますが、設定ファイルをjsファイルで作成しておくと変数やコメントが利用できて何かと便利です。

<details><summary>backstop.json(デフォルト)</summary><div>

```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
}
```
</div></details>

<details><summary>backstopConfig.js(少し調整済み)</summary><div>

```js
const baseUrl = "https://192.168.33.10/";
const referenceUrl = "https://192.168.33.11/";
const productPath = "test-product.html";

const onBeforeScript = "config/onBefore.js";
const onReadyScript = "config/onReadyScript.js";
const misMatchThreshold = 0.1;

module.exports ={
    "id": "backstop_default",
    "viewports": [
        {
            "label": "phone",
            "width": 320,
            "height": 480
        },
        {
            "label": "tablet",
            "width": 1024,
            "height": 768
        },
        {
            "label": "PC",
            "width": 1240,
            "height": 768
        }
    ],
    "onBeforeScript": onBeforeScript,
    "scenarios": [
        {
            "label": "Home page",
            "url": baseUrl,
            "referenceUrl": referenceUrl,
            "misMatchThreshold": misMatchThreshold
        },
        {
            "label": "Product Detail Page",
            "url": baseUrl + productPath,
            "referenceUrl": referenceUrl + productPath,
            "onReadyScript": onReadyScript,
            "hideSelectors": '#iframe-demo',
            "misMatchThreshold": misMatchThreshold,
        }
    ],
    "paths": {
        "bitmaps_reference": "backstop_data/bitmaps_reference",
        "bitmaps_test": "backstop_data/bitmaps_test",
        "engine_scripts": "tools/backstop/engine_scripts",
        "html_report": "backstop_data/html_report"
    },
    "report": ["browser"],
    "engine": "puppeteer",
    "engineOptions": {
        "args": ["--no-sandbox"]
    },
    "asyncCaptureLimit": 3,
    "asyncCompareLimit": 50,
    "debug": false,
    "debugWindow": false
};

```
</div></details>

重要そうな設定オプションはこのような感じ↓

- Viewports : 確認したい画像サイズを指定
- onBeforeScript : シナリオ実行前に動作させたいScriptを指定(Cookieのセットなど。)
- onReadyScript :　スクリーンショット取得前に実行させたいScriptを指定。(マウスオーバー時の挙動を確認したい時は、マウスオーバーをさせるjsをここで指定)
- scenarios : スクリーンショットを取得したいページを指定
 - url : backstop testコマンド時にスクリーンショットを取得する際のurl
 - referenceUrl : backstop referenceコマンド時にスクリーンショットを取得する際のurl
 - hideSelectors: スクリーンショット取得時に特定の要素を隠す(iframeなどの環境ごとに表示が変わるものをdispay:noneさせる)

詳しいオプションは[こちら](https://github.com/garris/BackstopJS#getting-started)

# 4. 画像ファイルの差し替え方法(for Magento2)

ECサイトのUIテストで頭を悩ませるものの一つが画像の扱いだと思います。

ECサイトの肝とも言える商品画像は、日々更新されていく上に、ほぼ全てのページに表示されているといっても過言ではないと思います。

商品点数の多いサイトで、全ての環境(テスト・ステージング・ライブ)で商品画像を合わせておくことは困難を極めるためUIテストの障壁の一つになってしまうかと思います。

BackstopJsでも、環境ごとに商品画像が異なる場合は、画像の差異が検出されてしまい、肝心のスタイル部分の変更がわかりづらくなってしまいます。。

<img width="616" alt="スクリーンショット 2018-12-15 19.49.08.png" src="https://qiita-image-store.s3.amazonaws.com/0/97920/54f5a459-57d2-65b9-74b8-af14c2d86de7.png">

BackstopJsでは、下記のスクリプト(interceptImage.js)をonBeforeScriptとして登録しておくことで、商品画像のリクエストを全てダミー画像に変更することができます。

interceptImage.jsでは、Magento2の画像ファイルが格納されているmedia/catalog/productディレクトリ配下の画像を参照しようとするリクエストを手元のdummy画像が表示されるように変更しています。

スクリプトは、インストール時にバンドルされてくる[Puppeteer](https://github.com/GoogleChrome/puppeteer)と呼ばれる、Headless Chromeを操作するためのJSライブラリを使用して書いています。

<details><summary>interceptImage.js</summary><div>

```js
const fs = require('fs');
const path = require('path');

const CATALOG_IMAGE = /\/media\/catalog\/product/i;
const IMAGE_URL_RE = /\.gif|\.jpg|\.png/i;
const IMAGE_STUB_URL = path.resolve(__dirname, './imageStub.jpg');
const IMAGE_DATA_BUFFER = fs.readFileSync(IMAGE_STUB_URL);
const HEADERS_STUB = {};

module.exports = async function (page, scenario) {
  const intercept = async (request, targetUrl) => {
    if (IMAGE_URL_RE.test(request.url()) && CATALOG_IMAGE.test(request.url())) {
      await request.respond({
        body: IMAGE_DATA_BUFFER,
        headers: HEADERS_STUB,
        status: 200
      });
    } else {
      request.continue();
    }
  };
  await page.setRequestInterception(true);
  page.on('request', intercept);
};

```
</div></details>

実行するとこのようなスクリーンショットを取ることができます。

<img width="600" alt="スクリーンショット 2018-12-15 19.58.24.png" src="https://qiita-image-store.s3.amazonaws.com/0/97920/5ea146f1-7572-7c25-e015-5bfd7e873340.png">

UIテスト実行時に画像差し替えのスクリプトを差し込むことで、画像が用意されていないテスト環境でも、本番環境もしくはステージング環境との比較テストを行うことができるようになります。

# 5. Magento2用のシナリオ作成

コンフィグファイルの設定方法・ダミー画像の表示切替が出来たので、ここからシナリオの作成方法を紹介していきます。

基本的にUIレグレッションテストでは、スクリーンショットを撮りながら全てのページを巡回していくことになりますが、マイページやチェックアウトページなどのスクリーンショットは、単にURLを叩いただけでは取得することができません。

その時に利用するのがonReadyScriptです。
onReadyScriptでは、Seleniumのようにブラウザ操作をするスクリプトを作成することでスクリーンショットを取る直前に様々な操作(ページ遷移・ボタンクリックなど)を挟むことができます。

BackstopJsでは、上述した通りデフォルトでHeadless Chromeがバンドルされてきます。
また、Headless Chromeを動かすためのライブラリが二つ標準搭載されています。

・Puppeteer
・Chromy

今回の記事では、Magento2でのログイン処理をPuppeteerで書いていきたいと思います。

PupetterのAPI仕様は[こちら](https://github.com/GoogleChrome/puppeteer/blob/v1.11.0/docs/api.md)

基本的な操作
>・page.waitForSelector　・・・ 指定したエレメントが表示されるまで待機
>・page.type ・・・ 指定したフィールドに特定のテキストを入力
>・page.click ・・・指定したエレメントをクリック
>・page.waitForNavigation ・・・ページ遷移後、DOMのロード・画像などの関連データの読み込みが完了するまで待機

##ログイン処理

ログイン処理スクリプトは以下のようになります。
async/awaitを利用すると、非同期処理が通常の同期処理のように記述出来てすごく便利ですね。
ただし、Pupetterの動きがどうも安定しない様子。。 (自分の書き方が悪いのか？:thinking:)

<details><summary>loginScript.js</summary><div>

```js
module.exports = async (page, scenario) => {
    await page.waitForSelector('#email');
    await page.type('#email',"xxxxx@example.com");
    await page.type('#pass',"zj4TH6Lq");
    await page.click('#send2');
    await page.waitForNavigation();
};
```
</div></details>

<details><summary>backstopConfig.js(マイページ表示用)</summary><div>

```js
const baseUrl = "https://192.168.33.10/";
const referenceUrl = "https://192.168.33.11/";
const loginPath = "customer/account/login";

const onBeforeScript = "config/onBefore.js";
const loginScript = "config/loginScript.js";
const misMatchThreshold = 0.1;

module.exports ={
    "id": "backstop_default",
    "viewports": [
        {
            "label": "PC",
            "width": 1240,
            "height": 768
        }
    ],
    "onBeforeScript": onBeforeScript,
    "scenarios": [
        {
            "label": "My Page",
            "url": baseUrl + loginPath,
            "referenceUrl": referenceUrl + loginPath,
            "onReadyScript" : loginScript,
            "misMatchThreshold": misMatchThreshold
        }
    ],
    "paths": {
        "bitmaps_reference": "backstop_data/bitmaps_reference",
        "bitmaps_test": "backstop_data/bitmaps_test",
        "engine_scripts": "tools/backstop/engine_scripts",
        "html_report": "backstop_data/html_report"
    },
    "report": ["browser"],
    "engine": "puppeteer",
    "engineOptions": {
        "args": ["--no-sandbox"]
    },
    "asyncCaptureLimit": 3,
    "asyncCompareLimit": 50,
    "debug": false,
    "debugWindow": false
};
```
</div></details>

```backstop reference --config=backstopConfig.js ```

これで、マイページのスクリーンショットを取得出来るようになりました。

ブラウザの動作が気になる方は、 コンフィグファイルの"debug","debugWindow"をtrueに変更することで、実際の動きを確認することが出来るようになります。

<img width="1411" alt="スクリーンショット 2018-12-16 18.01.47.png" src="https://qiita-image-store.s3.amazonaws.com/0/97920/a454693e-a77a-7785-a666-58fb2227e3a9.png">


# まとめ

今回の記事では、UIレグレッションテストツールであるBackstopJsをMagento2で使用する方法を解説させて頂きました。

本来であれば、チェックアウトページも含めて全ページで動作するものを紹介したかったのですが、Magento2のチェックアウトページではSPAライクな挙動があり、Pupetterの動作を安定させることが出来なったので、そのような内容は次回に持ち越しという形にさせて頂きたいと思います。　　m(__)m

次回の記事 :