IE のビジュアルリグレッションテストを BrowserStack Automate で自動化しよう

Internet Explorer をサポートしている限り動作確認をする必要がある.Mac を使っているので動作確認は Microsoft 公式の VM イメージVirtualbox を起動して確認か,BrowserStack で確認することになる.

BrowserStack は便利である.お金はかかるけれど WindowsiPadiPhone やいろんなリアルデバイス上で動かせるし,ローカルネットワークでしか繋げない開発環境もアドオンによって確認できる.

qiita.com

とはいえ毎回手動で BrowserStack でサポートブラウザを確認していくのがつらくなってきた.時間がめちゃかかる.

自動化

で,BrowserStack Automate というのがあると教えてもらったので,休みを使って自動化ツール作成を試したら,ぽいのができた:

BSBS Report: my test

f:id:mangano-ito:20200607232707p:plain
BrowserStack Automate をつかった BackstopJS もどき

github.com

BrowserStack × BackstopJS だから BSBS.いろいろと未実装が多くて余裕で未完成となっている.

BrowserStack Automate

BrowserStack Automate は Selenium でかんたんに動かすことができる:

www.browserstack.com

普通に BrowserStack を使うような WebDriver を作成すれば,通常の Selenium のテストそのまま操作できるので悩むことはない.ダッシュボードで操作ログも確認できて便利.

f:id:mangano-ito:20200607040651p:plain
BrowserStack Automate Dashboard のログ

ちなみに操作を動画で録画してくれててあとで確認できたりして楽しい:

f:id:mangano-ito:20200608010554g:plain
動画で操作が記録される

Firebase TestLab とかもこういう機能があって楽しい.

どういうデバイスで動かすかという Capabilities はページ上で設定を生成する:

www.browserstack.com

ここに BrowserStack の ID と キー も埋まってて,そのまま使うことができる.ローカルネットワークを使うかもここで設定できる:

var capabilities = {
    "os": "Windows",
    "os_version": "10",
    "browserName": "IE",
    "browser_version": "11.0",
    "browserstack.local": "false",
    "browserstack.selenium_version": "3.5.2",
    "browserstack.user": "<ID>",
    "browserstack.key": "<KEY>",
};

これさえ設定しておけば,公式のガイドに沿って実装すれば Selenium でらくらく操作ができる.おかしいときはさっきのダッシュボードのログを見てデバッグができる.便利.

ところで

今業務では BackstopJS を使って production と開発環境のスクリーンショットでビジュアルリグレッションテストをしている.これがとても便利で,幾度となく本番環境の破壊を防いでくれている.僕ではない先輩エンジニア方が導入してくださった仕組みだが,控えめに言って最高である.

で,BackstopJS のバックエンド(というべきなのエンジンなのかよくわからないが)は Puppeteer である.BackstopJS は Puppeteer でヘッドレスブラウザを操作して,テストシナリオどおりに要素の更新を待ったり,特定の要素だけをキャプチャして,最終的に reference (お手本) と test (比較したいもの) のキャプチャ画像を知覚的に比較している.

ここに Puppeteer 互換のレイヤーを差し込めるような仕組みがあれば,このドライバで動かすこともできるだろうが,詳しくは調べていない.ただエンジンは変更できるっぽい設定ファイルがあったりするので余地があるかもしれない.

ということで,逆の発想で BackstopJS 互換で BackstopJS のテストシナリオを入力すれば,BackstopJS っぽく BrowserStack Automate でキャプチャ比較してくれればいいのではないかと思った.

試す

というわけでテストしてみる.

英語版の Google と日本語版の Google を比較する.まあ,それなりに一致するだろうし,余裕でテキストの部分が不一致になるだろう.単純にトップの要素だけを比較するものと,検索欄にテキストを入れて検索ボタンを押すシナリオを準備した.

f:id:mangano-ito:20200608012604g:plain
Selenium で操作している

シナリオの操作は Selenium の操作になり,BrowserStack Automate のサーバーにコマンドが送られる.BrowserStack Automate は要求した Capabilities にあったリアルデバイス上のブラウザを起動して,コマンドどおりの操作をしてくれる.

f:id:mangano-ito:20200608012629g:plain
ダッシュボードも随時ログが記録される

これを reference と test それぞれキャプチャし,差分比較を走らせてることで冒頭のレポートが作成される:

BSBS Report: my test

試したテストシナリオは以下だ:

module.exports = {
    "id": "my test",

    "scenarios": [
        {
            "label": "google-top",
            "url": "https://www.google.com/?hl=en",
            "referenceUrl": "https://www.google.co.jp/?hl=ja",
            "readySelector": "input[name='q']",
            "selectors": [
                "#hplogo",
                "#searchform"
            ],
            "removeSelectors": [
                "input[name='btnI']"
            ]
        }, 
        {
            "label": "google-search",
            "url": "https://www.google.com/?hl=en",
            "referenceUrl": "https://www.google.co.jp/?hl=ja",
            "readySelector": "input[name='q']",
            "selectors": [
                "#top_nav",
                "#appbar",
            ],
            "keyPressSelectors": [
                {
                    "selector": "input[name='q']",
                    "keyPress": "Backstop + BrowserStack awesome"
                },
            ], 
            "clickSelector": "input[name='btnK']",
            "postInteractionWait": 100,
            "removeSelectors": [
                "#hdtb-tls",
            ]
        }, 
    ]
};

スクショを撮る

Selenium には要素のスクリーンショットを撮るコマンドがありそうだったが,実際に BrowserStack Automate でコマンドを流してみると 404 でコマンドが実装されていない感じだったので諦めた.

f:id:mangano-ito:20200606222114p:plain
要素のスクリーンショットはエラーになる

{
    "status" : 404,
    "sessionId" : "<no session>",
    "value" : "Command not found: GET /session/.../element/.../screenshot"
}

そのためページ全体のスクリーンショットを撮って,対象の要素だけを画僧処理でクロップするという代物になった.要素に他の要素がオーバーレイしている場合とかに困ったことになる.

とりあえずクロップには jimp という JS だけでできてる画像処理ライブラリを使ってみることにした.ネイティブのものにくらべパフォーマンスは劣るかもしれないが,機能が揃っていそうでお手軽である:

github.com

シナリオ

シナリオの仕様は BackstopJS と互換にする.なのでなるべくそれと同じ動作になるように実装する.

BackstopJS のシナリオを Selenium で操作する変換をすることが今回の主な仕事になっている.だから,実際に使いそうなユースケースはチェックしたけど,実は動かない,未実装のケースが余裕である.

たとえば BackstopJS では不要な要素を削除したり隠したりする機能があるけど,Selenium 側では単純に要素を取得してきて,生の JavaScript のコマンドを発行して element.style.display = "none" を実行したりしている.

readySelector という要素が DOM 上に現れるまで待つ機能は,Selenium にもある条件が満たされるかループごとにチェックして待つ機能があるので,これである要素が現れるまで待つようにしている.

あとはシナリオの実行前後に任意のスクリプトを実行したりする機能があるけど,まだ実装していなかったりする.

ローカル開発環境とクッキーと認証

セッションを実現したいときとかはクッキーを設定する必要がある.けど Selenium には Cookie を設定する API があるのでバッチリだ.実装したけど実は試してない.最悪すぎる.

BrowserStack はアドオンでローカルネットワーク経由できるので,開発環境の確認もできる,というのがある.ひとえに BrowserStackLocal を起動しておいて,ローカル経由で接続するフラグを追加して Selenium を開始することになる.

差分検知

一番肝心なのが画像の diff を撮ることだ.まあ,普通にピクセルごとの差分を撮ればいいのだけれど,BackstopJS の diff は賢くできているようだ.

github.com

BackstopJS の差分検知部分は別のモジュールになっていて,diverged というパッケージになっていることを発見した.これを見ると単純なピクセルマッチのアルゴリズムではない結果がある.移動を見ていて賢く差分を見ることができるようになっている.

https://github.com/garris/diverged/raw/master/docs/diverged%20images/changeGraphDiff.png

今回は戦術した jimp に差分の実装があったのでお手軽に使うことにした.しきい値も指定できるので便利にできている.

感想

大体は BackstopJS っぽく振る舞うように互換レイヤーもどきを書く仕事になった.Puppeteer の互換レイヤーを書いて BackstopJS に組み込むほうが早いんじゃないかって気がしてきたが,せっかく始めたのでとりあえずやることにした.

ざっくりお試して書いて興に乗ってズイズイと勧めてしまった.未実装や未確認の部分が多いので,業務で必要なくらいには実装しておけば少し助けになるかもと思った.運用はしていないのでいろいろとアラが出そうではある.

展望

BrowserStack Automate は使いまくるにはお金が必要になってくる.無料だと 300 分のクオータなので 5 時間実行したら終了となる.Pull Request ごととか git push ごとに比較を走らせる未来になったら,あっという間にクオータがやってきて比較できなくなる.

f:id:mangano-ito:20200608011615p:plain
あと 294 分しかない

お金を払うと制限がなくなったりパラレルで実行できるテストの数が増えて,今みたいに直列でやって鬼のように時間がかかる,というのも改善できるメリットもある.プランはこちらだ:

www.browserstack.com

少なからずお金がかかるけど,並列がなくてよければ月 1.3 万円ほどでリアルデバイスのテストを自動化できる可能性を秘めている.

ということで将来的に GitHub Actions とかで CI できればいいですね,みたいな世界観が広がっている.多分余裕でできると思われるが,GitHub Actions は気になっているが手を出してないのでやってみると面白そうだ.

ところで,Virtualbox をリモートで走らせて操作自動化とかできるだろうか.それができたら IE のイメージを使ってできなくもないかもしれないと思ったけど,ライセンス的にどうなのかという懸念もある.