dely Tech Blog

クラシル・TRILLを運営するdely株式会社の開発ブログです

1px の変化も見逃さない!ビジュアルリグレッションテスト導入で快適フロントエンド開発

こんにちは!dely でフロントエンドの開発をしています @all__user です。
今回は kurashiru のフロントエンド開発に導入されたビジュアルリグレッションテストについてご紹介したいと思います。

【反応を多くいただいた点について記事の最後に追記しました】

目次

ビジュアルリグレッションテストとは

ある変更を加える前後でスクリーンショットを作成し、それらを比較することで意図しない挙動が無いかを検証するテスト手法です。
最終的に描画されたピクセルに少しでも変化があれば検出することができるため、技術スタックを選ばず包括的にテストできます。
検出できるものは限られていますが、スタイルのチェックはもちろん、機能や動作を検証するためのテストとしても非常に優れています。

  • 検出できるもの⭕
    • お気に入りボタンが表示されない
    • 2ページ目以降がエラーページになる
    • 画像が荒い
  • 検出できないもの❌(検証に向かないもの)
    • お気に入りボタンがクリックできない
    • 2ページ目以降へ遷移できない
    • 画像のalt属性が設定されていない

現在の kurashiru のように Rails のテンプレートと Vue を併用しているような状況でも、全体をカバーできます。技術スタックの移行フェーズでは特に効果を発揮すると思います。
テストはどこにどれだけのコストを割くかというバランスが非常に難しいと感じます。
まず最初に導入するテストとしてビジュアルリグレッションテストはとてもおすすめです。

導入の背景

kurashiru は Rails アプリケーションとして Sprockets + Slim + jQuery + CoffeeScript + SCSS で開発されてきました。
ログイン機能の開発をきっかけに部分的に SPA (Single Page Application) を導入し、現在は Webpacker + Vue + TypeScript + SCSS での開発へと移行中です。

tech.dely.jp

そのようにして SPA 化を進めていく中で、機能開発が優先され、なかなかテストに手を付けられないという状況が続きました。
すでにある Capybara + RSpec によるテストケースだけでは、SPA 部分の開発が既存部分へ与える影響や共通モジュールを変更する際の影響の検知を担保できず、テストケースを増やして対応しようとすると、非常にコストがかかるだろうと考えられました。

なんとかしなければと思いつつ開発は続き、規模が少しづつ大きくなるにつれ、手動テストでの動作検証コストが無視できない大きさになってきました。
そして、今まさにテストが必要というところまで来ていると判断しビジュアルリグレッションテストを導入することにしました。

フロントエンドのテスト?

フロントエンドのテストは難しいです。
一口にテストと言っても何を検査し担保したいのかによってテスト手法もまちまちです。

  • ロジック
  • 操作
  • 要素、テキスト
  • スタイル
  • ブラウザ間の差異

HTMLの構造や見た目に依存したテストは、正しくテストを書く難易度も高く、しかも変更により壊れてしまう可能性も高いため、得られるメリットがコストに見合わないと感じることもあります。

SPA移行前後の比較

今回一番達成したかった目的は、SPA移行前後のコードに対する検証です。
何かしらのテストの必要は感じていましたが、移行前後のコード全てに対してテストを追加するコストはかけられません。

ビジュアルリグレッションテストは期待される動作の定義と検証を簡単に行うことができます。
期待される動作の定義はスナップショットを撮るだけです。
現在の状態を期待される動作として定義し、移行後の状態と比較することで、移行前後のコードの挙動に変化がない(またはある)ことを確認できます。

ツール

調べてみたところ様々なツールがありました。
PhantomJS は開発が終了しているため、それをベースにしたツールは今回選択肢から外すことにしました。
また、マルチブラウザのテストを考慮すると Selenium ベースが望ましかったのですが、学習コストやフロントエンドエコシステムとの相性の観点から Node.js, Puppeteer を採用している BackstopJS を採用することにしました。

reg-suit

スナップショットの比較とレポートの作成に特化したツールです。
スナップショットの生成をどのように行うかは自由なため、Headless Chrome を使ったり Selenium ベースのツールを使うなど柔軟に対応できそうです。
CIにも組み込みやすそうです。

github.com

Loki

Storybook に特化したツールです。
スナップショットの生成からレポートの作成、結果の承認まで全部入りのツールです。
ページ全体のスナップショットを利用するテストとは違い、コンポーネントの単体テストという位置づけです。
kurashiru でも最近 Storybook が導入されたのでいずれ試してみたいです。

github.com

Wraith

Ruby ベースのツールで、GitHubのスターも一番多いようです。
スナップショットの生成からレポートの作成、結果の承認まで全部入りのツールです。
メインは PhantomJS ベースのようですが Chrome にも対応しているようです。
BBC News が作っています。

github.com

BackstopJS

スナップショットの生成からレポートの作成、結果の承認まで全部入りのツールです。
今回はこのツールを採用しました。
Node.js 製でメインのブラウザに Puppeteer を採用しています。
Puppeteer の API をそのまま利用できるので、テストケースを async / await で書くことができます。
hideSelector や removeSelector で特定の要素を非表示にしたり取り除いたりすることができたりと、いい感じのパラメータが多く用意されています。

github.com

テストのフロー

f:id:delyjp:20180802000550p:plain

GitHub + CodeBuild + BackstopJS

CodeBuild の GitHub 連携の機能を利用し、特定のルールにマッチするブランチ名が push された時にテストが走るように設定しました。
CodeBuild ではテストに使用する Docker イメージを指定することができるのですが、BackstopJS が提供している Docker イメージがあり、これを利用しています。
これで CI 環境 で Puppeteer を動かすための手間はかかりません。
一つだけ注意が必要なのは、そのままだと日本語フォントが入っていないため、フォントの入ったイメージを作るか、install フェイズなどでフォントを入れる必要があります。

version: 0.2

phases:
  install:
    commands:
      - apt-get update -y
      - apt-get install -y apt-transport-https
      - curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | sudo apt-key add -
      - echo "deb https://dl.yarnpkg.com/debian/ stable main" | sudo tee /etc/apt/sources.list.d/yarn.list
      - apt-get install -y yarn
      - apt-get install -y fonts-ipafont-gothic fonts-ipafont-mincho # 日本語フォントをインストール
      - apt-get install -y python-dev
      - curl "https://bootstrap.pypa.io/get-pip.py" | python
      - pip install awscli

  pre_build:
    commands:
      - REGRESSION_TEST_BRANCH_NAME=$(git branch -a --contains $CODEBUILD_SOURCE_VERSION)
      - mkdir -p ./.yarn-cache
      - yarn install --cache-folder ./.yarn-cache

  build:
    commands:
      - yarn reg:testcase-gen # スプレッドシートから backstop.json を生成
      - backstop reference # リファレンスのスナップショット
      - backstop test # テストのスナップショット

  post_build:
    commands:
      - aws s3 cp --recursive backstop_data/html_report s3://xxxxxxxxxxxxxxxx/$CODEBUILD_BUILD_ID/html_report/
      - aws s3 cp --recursive backstop_data/bitmaps_reference s3://xxxxxxxxxxxxxxxx/$CODEBUILD_BUILD_ID/bitmaps_reference/
      - aws s3 cp --recursive backstop_data/bitmaps_test s3://xxxxxxxxxxxxxxxx/$CODEBUILD_BUILD_ID/bitmaps_test/
      - yarn reg:slack # Slackに通知

cache:
  paths:
    - '.yarn-cache'
    - 'node_modules/**/*'

ステージング環境

Reference と Test には kurashiru の開発で利用しているステージング環境を使用しています。
開発者が任意のブランチをデプロイできるようになっています。
データベースの内容によってスクリーンショットに差が出ないように、ビジュアルリグレッションテスト用のインスタンスは同じデータベースを参照するようにしています。

テストケースは Google スプレッドシートで管理

BackstopJS のテストケースは backstop.json というファイルの scenarios プロパティで設定します。

{
  "scenarios": [
    {
      "label": "recipes_show",
      "onBeforeScript": "src/on_before.js",
      "url": "https://example.com/recipes/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
      "referenceUrl": "https://example.com/recipes/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
      "delay": 3000,
      "removeSelectors": [
        ".adsbygoogle",
        ".google-ads"
      ],
      "onReadyScript": "src/on_ready.js",
      "selectorExpansion": true,
      "misMatchThreshold": 0.1,
      "requireSameDimensions": true
    }
  ]
}

もちろんこのまま利用してもよいのですが、backstop.json が巨大になるとメンテナンスが大変そうなので、Google スプレッドシートでテストケースを管理することにしました。

f:id:delyjp:20180802000659p:plain

一行が一つのテストケースに対応しており、上記の JSON の各種パラメータを設定できるようにしています。
また、パタメータとは別に自由に設定できるタグ用のカラムを追加しました。
このタグを利用して、特定のテストケースのみを実行したり、どの画面か、管理サイトかなどで色分けできます。

テスト開始前にこのスプレッドシートのデータを取得し backstop.json を生成します。

各テストケースが見やすく編集もしやすくてとても便利なのですが、テストケースが Git の管理下ではなくなるというデメリットもあります。

結果を S3 にアップロードして Slack に通知

BackstopJS はテスト結果を HTML + アセット群というかたちで生成します。
このようにスクリーンショットの比較結果をとても見やすく表示してくれます。

f:id:alluser:20181207182333p:plain

f:id:alluser:20181207182750p:plain

この HTML + アセット群を静的サイト向けにセットアップした S3 にアップロードし、Slack に URL を通知します。

まとめ

SPAへの移行では実際に多くの意図しない挙動を1px単位で見つけることができました。
このタイミングで導入できてとても良かったです。

ビジュアルリグレッションテストは他のテストを代替するようなものではないですが、技術スタックに依存しないことと、テストケースが壊れにくいというところが大きな利点かと思います。
ビジュアルリグレッションテストを導入して快適なフロントエンドライフを送りましょう!

【追記】

多くの方に読んでいただきとても嬉しいです🙌反応をいただいた点について追記しました!

運用が大変ではないか?

現状、リグレッションテストに関しては、ユニットテストやE2Eテストのように、コミット度にテストを回して、テストケースが全てパスしないとマージできない、という運用はしていません。

雰囲気としては、

「今回だいぶ変更箇所多くなったなー、一応リグレッションテスト見ておくか」
「うわ、差分めっちゃ出てる、こりゃよく分からんね...」
「そうすね、今回はざっと見て問題なさそうだったら、新しいほうをリファレンスにしちゃって下さい」
「了解です!大丈夫そうです!」
「マージ!」

こんな感じです。

テスト実行の有無は都度判断し、もやは有益でない差分については無視しても構わないくらいの運用です。
通常のテストとは違いこのくらいの運用でも十分効果があります。

1pxの違いにそこまで工数かける?

上記のような運用方法が前提であれば、そもそもこの点は解消するかもしれません。

一つの例として1pxの線が挙げられると思います。
デザイン上1pxの線が使われている部分はkurashiruにも多くあります。
1pxのズレで大きなデザイン崩れになることは無いかもしれませんが、1pxの線でも、その線が消えてしまうと大きく意味が変わってしまう、ということはよくあると思います。

その線が誤って消えてしまったりすると、

「すいません!ここの線が消えちゃってました!」
「あっ!すいませんすぐ修正しますー(たぶんあの変更だ...)」

のように、そのページを見て気づいた人からの報告ベースでの修正ということはよくあります。(それ自体は良いと思います🙌)
要は、開発者や関係者が手動で目grepで確かめないといけなかった部分を、ある程度置きかえられるので、ページ数にもよりますが、費用対効果としては悪くないと思います。

広告が差し込まれたり変わっただけでテストが壊れるのでは?

これはその通りなのですが、BackstopJSの機能で対応可能です。
テストケースごとに特定のセレクタにマッチした要素をvisibility: hiddenにしたり、display: noneすることができます。
下のリンク先にあるhideSelectorsremoveSelectorsというオプションです。

github.com

他にも色々な機能があるので、ある程度のケースには対応できると思います。