制作現場におけるビジュアルリグレッションテストの導入 – 「LINEのお年玉」4年目の挑戦

はじめに

みなさんこんにちは。LINEのフロントエンドエンジニアの藤井です。年末年始に実施した「LINEのお年玉」キャンペーンも今年で4回目の実施となりました。
今年は過去4年で経験した様々な反省点を振り返り、様々な改善を行いました。今回はそのなかの一つである「ビジュアルレグレッションテスト」の実施についてご紹介します。この記事では、ビジュアルレグレッションテストを運用していく上で工夫したことや、得られた知見などを共有したいと思います。

案件の特徴を紹介

まずは「LINEのお年玉」キャンペーンの特徴をご紹介します。

デコレータ使わない Vue.js + TypeScript で進んだ「LINEのお年玉」キャンペーン「LINEのお年玉」におけるフロントエンドでの UX へのこだわりでも紹介されていますが、毎年年末年始に行われるキャンペーンとして、トラフィックが多いアプリケーションを、短期間で制作することが求められる案件となります。

LINE のフロントエンドでは一つの Web サービスとして成り立つ SPA を組むことが多いです。それに加えてこのプロジェクトでは以下のような特徴があります。

  • 1月1日 のリリースが確定しているので、厳しいスケジュールの中、スピードを重視した開発が求められている
  • デザインや仕様について直前まで多くの改善が行われており、新しい仕様への追従が頻繁に起こる

これらの特徴によって毎年手戻りや仕様の食い違いが発生し、リリース直前までコードを修正しているという状況が続いていました。

ビジュアルレグレッションテストの導入

そこで今回は、細かな UI の変更漏れを最小限に抑え、QAフェーズにおけるバグレポートの数を少しでも減らすための施策として、ビジュアルレグレッションテストを導入しました。

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

ビジュアルレグレッションテストとは名前の通りページの見た目をテストするための仕組みです。アプリケーション実行時の画面のスナップショットを撮影し、あらかじめ用意しておいた正解のスナップショットを比較するというテストです。ピクセル単位で色情報を比較し、異なっている部分があるとテストが失敗するという仕組みです。

ビジュアルレグレッションテストと比較して通常ユニットテストでは、

  • 実行時のDOMのスナップショットを記録し正解値と比較する。
  • DOMの中からStringを取得し正解値と比較する。

といったテストを実施することが多く、部分的なテストに留まってしまいます。しかし、ビジュアルレグレッションテストは、ページ全体の「見た目」をテストすることが可能となります。従来のテスト手法では難しかった、スタイルに関してもテストでカバーできるというメリットがあります。ビジュアルレグレッションテストはほぼ完璧なテストが可能となる反面、1ピクセル単位の比較となるため、少しのスタイル調整や仕様変更などで簡単にテストが失敗してしまうという特徴があります。テストが失敗すると再度正解となるスナップショットを撮影し、それを正解のスナップショットとして登録するなどの作業が発生します。したがって、メンテナンスコストが高いといった理由から、通常は成熟したプロダクトの保守運用フェーズで導入されることが多いです。

制作のフェーズでビジュアルレグレッションテストを導入した背景

このようにUIが頻繁に更新される「制作」のフェーズではビジュアルレグレッションテストとの相性が悪いのですが、今回はあえて制作のフェーズから導入してみました。導入を決めた理由は以下の通りです。

  • ユニットテストを実施する場合は、コンポーネント毎にテストが必要となり、テストの数が増大します。しかし、ビジュアルレグレッションテストは一つのテストで多くのことをカバーできます。
  • 「LINEのお年玉」を起動した際にランディングページのパターンが数多く存在し、毎回全てを目視で確認することが難しい。
  • 通常のユニットテストではDOMに埋め込まれた文字列を取得し、正解値と比較するテストが実施されることが多いですが、今回はテキスト自体が画像になっているものが多く、このような手法でテストが困難でした。
  • 複数人で共通コンポーネントを開発しているため、コードをマージする前に意図していない副作用がないかを確認したかった。

そして今回は、テストのメンテナンスコストを極限まで下げるため、正解として扱うスナップショットを更新するプロセスを簡略化するための仕組みを考案し、制作のフェーズからビジュアルレグレッションテストができるような仕組みを構築しました。

導入したパッケージ

ビジュアルレグレッションテストを導入するにあたって、今回は jest-puppeteer、 jest-image-snapshotexpress を導入しました。まず、Jest と Puppeteer について簡単に紹介します。Jest とは、Facebook 社が開発しているユニットテストを行うためのテストフレームワークです。テストを実施するために必要な、テストランナーやアサーションなどの便利な機能を一通り提供しています。Puppeteer は、UIをもたない headless Chrome です。Node.js の環境で動作し、ブラウザを操作するための様々なAPIを提供しています。

jest-puppeteer によって puppeteer の API を呼び出し、レンダリング結果をベースに、 jest-image-snapshot を利用してスナップショットベースで画像を比較するという形です。
jest-image-snapshot は、 Jest の toMatchSnapshot とほぼ同じ書き心地で、画像をピクセル単位で比較を行ってくれるパッケージです。そして最後に、express によって Node.js の Mock API サーバを実装しました。

テスト実施からテスト結果通知までのフロー

今回は CircleCI 上でビジュアルレブレッションテストを実施し、その結果を Slack に通知する仕組みを構築しました。テスト実行結果を Slack に通知するため、CircleCI からの Webhook を受け取りそれを Slack に通知するための Chat Bot を実装しました。

テストの実行から、テスト結果を取得、最後に Slack へと通知するまでの具体的なフローは次の通りです。

  1. 開発者がリモートのリポジトリにコードをプッシュする。
  2. CircleCI が GitHub から Webhook を受け取り、ビジュアルレグレッションテストの Job がトリガーされる。
  3. ビジュアルレグレッションテストが失敗すると差分を記録したスナップショットが社内の共有サーバへアップロードされる。
  4. Slack Chat Bot は CircleCI から Branch Name、Job Name、差分を記録したスナップショットの URL などの情報を受け取る。
  5. Chat Bot は、差分を記録したスナップショットのURLやビジュアルレグレッションテストの実行結果をSlackのチャンネルに通知する。開発者はその通知メッセージから、差分を記録したスナップショットが確認できる。

Slack に通知されたメッセージからテスト結果確認のボタンをクリックすると、実行結果と期待する画像との間にこのような差分があることが確認できました。真ん中の画像が差分を可視化した画像となっており、相違がある部分が赤色で表示されています。今回のケースでは、ポップアップが表示されることが期待されていますが、実際の実行結果ではポップアップが表示されていません。しかし、これはバグではなく仕様変更における正しい結果です。そのため、正解のスナップショットを更新する必要があります。(正解のスナップショットを更新するフローは後述します)

CircleCIでビジュアルレグレッションテストを実行するコード

これでひとまずは完成しましたが、 テストケースの増加に伴い、ビジュアルレグレッションテストの実行時間が肥大化してしまいます。ここでは、テストの実行時間を短縮するために行った工夫を二つ紹介します。一つ目は Job の並列実行です。CircleCI には Workflow という機能が提供されており、これを使うことでテストの実行時間が肥大化する課題を解決することができました。Workflow とは、Job の実行順序や並列実行などの実行時のルールを記述することで、そのルールに従って CircleCI が Job を実行してくれる機能です。今回のケースでは、テストケース毎に Job を分割し、全てのテストケースを並列実行するよう Workflow に記述をすることで、高速なテスト実行を実現しました。二つ目は実行環境のキャッシュです。CircleCI には特定のファイルやディレクトリをキャッシュする機能が提供されています。今回はインストールに長い時間を要する npm modules をキャッシュして、環境構築に要する時間を短縮しました。

テスト実行の流れは次の通りです。デフォルトで日本語がサポートされていないので、日本語フォントをインストールする必要がありました。

  • キャッシュから実行環境を復元
  • 日本語フォントのインストール
  • Mock API サーバを起動
  • ビジュアルレグレッションテストを実行
  • ビジュアルレグレッションテストの実行に失敗した場合は画像を共有サーバにアップロード
  • 実行環境をキャッシュする
test1:
  docker:
    - image: circleci/node:10.16.0-browsers
  steps:
    - checkout
    - restore_cache:
        keys:
          - v1-dependency-cache-{{ .Branch }}-{{ checksum "package.json" }}
          - v1-dependency-cache-{{ .Branch }}
          - v1-dependency-cache
    - run: npm install --build-from-source
    - run: sudo apt -qqy --no-install-recommends install -y fonts-takao-gothic fonts-takao-mincho &&
        sudo dpkg-reconfigure --frontend noninteractive locales &&
        sudo fc-cache -fv
    - run:
        command: MODE=test1 npm run server
        background: true
    - run:
        command: npm run serve
        background: true
    - run: npm run test:e2e -- ./tests/e2e/specs/test1.test.js --outputFile=test1
    - run:
        command: npx reg test
        when: on_fail
    - save_cache:
        key: v1-dependency-cache-{{ .Branch }}-{{ checksum "package.json" }}
        paths:
          - node_modules

運用にあたって必要だったこと

当初はテストが失敗するとローカル環境で正解のスナップショットを撮影し、リモートリポジトリにプッシュするといった方法を想定していました。しかし、実際に運用してみるといくつかの問題がありました。

実行環境の違いによる差分の発生

テストの実行環境と正解のスナップショットを撮影した環境に違いがあることが原因で次のような問題が生じました。

  • スナップショットの解像度の違いによる差分の発生
  • OS 間のシステムフォントの違いによる差分の発生

そこで、テストの実行環境と正解のスナップショットを撮影する環境を同一にすることで、実行環境の差分による予期せぬ差分を防ぎました。ビジュアルレグレッションテストは CircleCI 上で実行することを想定していたので、比較対象とするスナップショットの更新も CircleCI 上で行うことにしました。また、スナップショット更新のコストを最小限にするため、ボタン一つでリモートの正解のスナップショットが更新される仕組みを構築しました。

画像更新のフロー

正解として扱うスナップショットを更新するフローは次の通りです。

  1. 開発者は Slack Chat Bot が通知したメッセージ上に表示されているスナップショット更新ボタンをクリックします。
  2. Slack Chat Bot は画像更新のアクションを受け取り、CircleCI の画像更新 Job を起動するための API を呼び出します。
    • CircleCI にはJobを起動するためのAPIが提供されています。API を呼び出す際に、Job 名、CircleCI のトークン、ブランチ名などの変数を渡してJob を起動します。
  3. CircleCI は新規に記録した正解のスナップショットをGitHubのリモートリポジトリにプッシュします。

テスト環境のデータが頻繁に更新されてしまいビジュアルレグレッションテストが失敗する

ビジュアルレグレッションテストを実行する際、当初はテスト環境にデプロイされているサーバ API を利用していましたが、テスト環境は企画者、サーバ開発者、QA担当者と共有しており、CMS上から自由にデータを変えることができてしまいます。
データが変わると UI も更新されてしまうので、その度に正解のスナップショットを更新していました。しかし、更新頻度が高すぎて、開発者の修正に関係なくビジュアルレグレッションテストが頻繁に失敗してしまうという問題がありました。

そこで、テストデータを固定するため、ビジュアルレグレッションテスト用の Mock API を実装しました。テストデータを固定したいとは言いつつも、様々なパターンでテストしたかったので、環境変数に応じてレスポンスを切り替えられるように実装しました。
例えば、お年玉送信済みリストの確認ページのでは、送信済みリストがゼロ件のパターンと、100件のパターンでビュジュアルレグレッションテストを実施する必要がありました。従って、Mock API サーバ起動時に次のような環境変数を与えることで、環境変数によって適切なレスポンスを返すようにしました。

MODE=LIST_ZERO npm run server

Mock API サーバは CircleCI の Job の中で起動し、バックグラウンドのプロセスで動かしました。本プロジェクトではvue-cli3 を利用しており、vue.config.js 内でサーバ API エンドポイントの変更が簡単に行えました。

ビジュアルレグレッションテストを導入したことによる効果

最後に導入した成果についてです。過去3年間で指摘されたバグの数を比較してみます。過去3年の「LINEのお年玉」での指摘事項数は、以下のようになっていました。これにはバグのほかに、 UI の仕様変更の追従などを含みます。

バグ数 (仕様変更も含む)
2018104
2019112
202076

2018年、2019年と比較し、2020年は30%ほどバグが少なくなっていることがわかります。毎年内容やアプリケーションの規模も変わるため一概には比較できませんが、一定以上の効果があったように見えます。実際コードデプロイ前に、ビジュアルレグレッションテストを実施することで、事前にデグレを発見することができ、効果の高さを実感することがきました。また、今年はデグレが少なかった分、例年課題となりがちなエッジケースでのテストに時間を割くことができ、プロジェクトとして余裕を持ったスケジュールで進行できました。

まとめ

今回は「LINEのお年玉」キャンペーン4年目の取り組みとして、制作フェーズにおけるビジュアルレグレッションテストを紹介しました。

制作フェーズにおけるビジュアルレグレッションテストの導入は、UI の変更が頻繁に発生するため、出来るだけ低コストでのメンテナンスが求められます。今回は、ボタン一つでテストを最新の状態にメンテナンスする仕組みを考案したこと。小規模なアプリケーションであったことが幸いし、破綻することなく運用できました。今回のように、文字自体が画像になっている UI は、DOM ベースのテストがやりにくいという課題があるため、ビジュアルレグレッションテストは最適なテスト手法であったと思います。

一方で、アニメーションが使われている画面では、ピクセル単位のテストではうまく解決できずビジュアルレグレッションテストが導入できないという課題が残りました。実行時に毎回微妙に UI が変化してしまうようなアプリケーションとは相性が良くないという知見を得られました。
スケールするアプリケーションに対してどのようにメンテナビリティを担保するか、ユーザー体験とテスタビリティの両立のためにどこまでをサポートするかの検証を続けていきます。

LINE株式会社では、プロダクトとコードの両面から品質を担保したいフロントエンドエンジニアを募集しています。
ご興味のある方は、以下のリンクよりぜひご応募ください。
https://linecorp.com/ja/career/position/475

Related Post