こんにちは。Yahoo!広告 ディスプレイ広告(以下、ディスプレイ広告)エンジニアの今宮です。アプリへの広告配信を行うシステムの開発と、Webサービス向けに広告を表示するためのタグ(以下、広告タグ)の開発を担当しています。
旧来リリースを行うタイミングでは、手動でいくつかのWebサービスの目視確認を行っていました。そこでVisual Regression Testingを導入することで自動化し、工数削減することができました。今回は実例を示しながら、この実現方法をご紹介します。
ディスプレイ広告の広告描画について
ディスプレイ広告が画面に描画されるまでの処理フローは以下のようになります。
- Webサービスに実装されている広告タグ内のスクリプト(以下、広告スクリプト)を、scriptタグを利用し読み込む
- 広告スクリプトが、広告配信設定システムへリクエストを送信し、 表示を行う広告のランキング付きのリストを受け取る
- 表示可能な広告のレスポンスを受け取るまで、ランキングの上から各社のSSP(クライアント側に表示する広告を返すシステム)へ逐次的にリクエストを送信する
- SSPから受け取ったレスポンスを元に、Webサービスに広告を描画する
広告タグの開発では単体テストや、 広告描画を確認するための簡単なWebページを利用したE2Eテストを行ってますがそれらだけでは、Webサービス側で表示されているタグの組み合わせやスタイルの兼ね合いなどで想定外のデグレが出る可能性があります。
広告タグは、ヤフーサービス各種で利用されており、各サービスで正しく広告が配信されているかを確認する必要があるため、リリースを行うタイミングで、手動でいくつかのWebサービスの目視確認を行ってました。その組み合わせに対して、広告側で全パターン用意するのは不可能なので、リリースを行う前にヤフーのいくつかのサービスに対して目視で、全ての広告枠が正常に表示されているかどうかを確認することで、デグレ防止をおこなっていました。
これですと、目視確認するサービスの数によって目視確認にかける時間が膨れ上がり、一度の目視確認に30分から1時間ほど要していました。
そこでVisual Regression Testingを導入することで、工数削減に取り組みました。
Visual Regression Testingについて
今回上述した、目視確認の工数削減を行うために、Visual Regression Testing(以下、VRT)を利用します。
一般的なVRTフロー
一般的なVRTの処理フローは以下のようになります。
- 修正が行われる前のページ/コンポーネントの画像を保存する
- コードの修正が行われたページ/コンポーネントの画像を保存する
- 1と2の画像を比較し、差分が生じているかを確認する
広告スクリプトのためのVRTフロー
上記行程を広告の表示で行う場合、正しく広告が表示されている本番運用中Webサービスと、今回修正を加えた広告スクリプトを利用し本番運用しているWebサービスの、2つの画面に対してVRTを行う必要があります。
また、今回のVRTフローを行う上で、2点ほど課題点があります。
- Webサービスが表示する広告が一定ではなく、異なる広告が引き当たってしまいVRTが失敗する
- Webサービス側のUI変更でVRTが失敗する
課題点を解消するために、下記のフローを行います。
- 広告タグが利用されている本番運用されているWebサービスのページの、広告タグに関するリクエスト/レスポンスを保存する
- 1で保存したリクエスト/レスポンスをスタブした上で、本番運用されているページを表示する
- 広告要素以外に対して、
visibility: hidden
を利用し、非表示にし保存する - 1で広告スクリプトのレスポンスを今回修正した広告スクリプトに差し替える
- 4で保存/修正したリクエスト/レスポンスをスタブした上で、本番運用されているページを表示する
- 広告要素以外に対して、
visibility: hidden
を利用し、非表示にし保存する - 3と6の画像を比較し、差分が生じているかを確認する
これらの行程を踏むことで、課題点を解消し、広告スクリプトの差分比較ができました。
フロー詳細
今回利用した言語/ライブラリ
まず、今回利用した技術スタックについて共有しておきます。
- テストツール
- jest
- puppeteer
- 画像関連
- pixelmatch(外部サイト)
- pngjs(外部サイト)
実装
広告スクリプトのためのVRTフローに関して、説明のために下記の階層ごとに分類します。
- ネットワーク層
- 広告タグが利用されている本番運用されているWebサービスのページの、広告タグに関するリクエスト/レスポンスを保存する
- 表示層
- リクエスト/レスポンスをスタブした上で、本番運用されているページを表示する
- 広告要素以外に対して、
visibility: hidden
を利用し、非表示にし保存する
- 比較層
- 画像を比較し、差分が生じているかを確認する
こうすることで、フローは下記のように記述できます。
- ネットワーク層 → 表示層(正解画像) → 広告スクリプト差し替え → 表示層(対象画像) → 比較層
このフローを擬似コードに落としたものが下記です。
import { readFileSync } from 'fs';
import { Page } from 'puppeteer';
type NetworkResponse = {
header: Record<string, string>;
status: number;
buffer: Buffer;
} // networkで再利用したい要素 bodyなど
type NetworkObject: Record<string, NetworkResponse>
type ReachType = {
targetURL: string,
reachFunction: (page: Page) => Promise<void> // targetURLでページを開いた後に、reachFunctionを行う
} // 最終的に撮影したい場所まで到達するためのオブジェクト
const reach = {targetURL: "https://yahoo.example", reachFunction: () => {}}
const AD_SCRIPT_URL = "https://yahoo.resource.co.example/ad-script.js" // 広告スクリプト
const AD_SCRIPT_DIR = "/dist/ad-script.js" // 開発環境下でビルド結果
const networkObject: NetworkObject = captureADNetwork(reach) // ネットワーク層
const prodPageImage = await screenShotPage(reach, networkObject) // 表示層
networkObject[AD_SCRIPT_URL].buffer = readFileSync(AD_SCRIPT_DIR); // スクリプト入れ替え
const devPageImage = await screenShotPage(reach, networkObject) // 表示層
const diff = await isVRTDiff(prodPageImage, devPageImage) // 比較層
expect(diff).toBeFalsy();
各層ごとに、詳細を見ていきます。
ネットワーク層
ネットワーク層では、reachオブジェクトを引数として受け取り、対象ページの対象場所へ遷移する過程をNetworkObject
として返します。
こちらでは特定の本番ページのネットワークレスポンスを取得します。
執筆時点の広告システムでは、自社のSSPのみに絞り込みをかけた上で、VRTを行うようにしています。広告配信設定システムのテスト環境では、広告を表示する枠に対して、その枠のユニークIDを取得し、専用のクッキーを利用することで、SSPの絞り込みを行えるようにしています。それを利用しSSPの絞り込みを行います。
表示層
表示層では、ネットワーク層
で取得したNetworkObject
を利用し、広告に関するネットワークをスタブした上で、対象ページの対象場所へ遷移します。
この際に広告要素以外のものを表示して、差分が生じた場合にそれが必要な要素がどうか判別できません。そのため、下記のように実装し、広告要素以外を非表示にしました。
// NetworkResponse[]から広告要素を抜き取りリストで返す関数
const getAds = (Response[]) => {
...
}
const addPageStyles = async() => await page.addStyleTag(`${selectors.join(',')}{${styles
.map((style) => `${style.property}: ${style.value};`)
.join('')}}`);
const filtedTargetSelectors: string[] = getAds(Object.values(networkObject).filter(networkResponse => isSSP(networkResponse)))
await addPageStyles(
page,
[{ property: 'visibility', value: 'hidden !important' }],
'*'
);
await addPageStyles(
page,
[{ property: 'visibility', value: ' visible !important' }],
...filtedTargetSelectors,
...filtedTargetSelectors.map((pelem) => pelem + ' *')
);
ページ全体をhiddenにし、広告要素のみをvisibleに上書きすることで表示するようにしています。
また、この方法ではWebサービス側の依存の問題で、広告要素のx,y軸が移動しないことが前提です。そのため、移動してしまう場合に下記の方法で対処することを考えました。
Webサービス側初回遷移時に、レンダリング結果後の計算済みスタイル、HTML要素をすべて保存し、二回目以降遷移時にその結果を利用する
- メリット: 一度そのシステムを作れば、どのWebサービスで実行しても移動が生じにくい
- デメリット: 実装が大変
Webサービス側の依存が出る箇所を、都度特定のスタイルで上書きする
- メリット: 実装が楽
- デメリット: スタイルの変更箇所が多いと、本番環境との差異が出てしまい、信頼性が低下する
Webサービス側でx,y軸の移動が出ないように、Webサービス側のフラグ設定や特定のネットワークレスポンスをスタブするようにする
- メリット: 1に比べて実装が楽、本番環境との差異も2に比べて少ない
- デメリット: Webサービス側の協力を仰ぐ必要がある
1のようにどのWebサービスでも適応可能な実装が理想ですが、目視確認で確認していたサービスの中ではWebサービス側の依存で広告要素のx,y軸が移動していたものは多くなかったため、一番簡単に実装可能な2で実装しました。
比較層
こちらでは、pixelmatchを利用し、表示層で取得した二つの画像の差分を出します。
実行結果例
ヤフーIDのログイン画面を例に、実際に広告要素の差分が出ないパターンと、差分が出るパターンを見ていきます。
表示層によって、広告画像のみが切り出された画像が比較層に送られ、
現在リリースされている広告スクリプトと、開発中の広告スクリプトに差分が生じる場合、テストを失敗させます。
ここで試しに、
networkObject[AD_SCRIPT_URL].buffer = Buffer.from("")
として、広告スクリプトを空にします。
そうすると、開発中の広告スクリプトを差し替えた環境では、下記のように広告が表示されません。
この際に、テストが失敗し、下記の画像が保存され、
PRなどで確認し問題ない差分かどうかを確認します。
まとめ
今回は、従来目視確認で行っていた作業を自動確認に代替できました。
VRTの課題点としては、他の自動テストと比べ、画像の差分を扱うため表示層で遭遇した関係ない差分が発生する問題や、Google Chromeを実際に立ち上げ、差分を確認する必要があるので、実行時間と、実行リソースを大幅に消費してしまう問題などがあります。(今回のものでは1テストケースで平均5分、実行環境/ケースによっては10分〜15分程度かかる場合もあります。テストファイル単位では、15分〜30分程度かかる場合があります。)
いくつかの課題点がある以上、どのようなシステムにも適応できるわけではありませんが、他の自動テストで代替できない手動テストに対する方法の一つとして、十分活用できるのではないかと考えています。
今後は、VRTの対象とするWebサービスを増やしていきたいので、表示層で説明した1の「Webサービス側初回遷移時に、レンダリング結果後の計算済みスタイル、HTML要素をすべて保存し、二回目以降遷移時にその結果を利用する」を行いどのようなサービスにも、信頼性の高いテストができるようにしていく予定です。
こちらの記事のご感想を聞かせください。
- 学びがある
- わかりやすい
- 新しい視点
ご感想ありがとうございました