reftest を導入しプロダクト品質改善の土台を整えた話


はじめに

ニコニコサービス本部ニコニコ開発部の小野寺と田中です。ニコニコ生放送の開発をしています。 今回は我々のチームで担当しているプロダクトの「品質改善の事例」をご紹介します。

Web アプリケーションの reftest (Reference Test) を導入する事例が出てきます。興味のある方はぜひご一読ください。

reftestサンプル

どんな課題があったか

今回ご紹介する事例は、 ある Web アプリケーションフレームワークの品質改善です。 過去の記事でもご紹介した Akashic Engine と呼ばれるもので、以降は 「エンジン」 と呼称します。

エンジンには 二つの課題 がありました。 いずれもある時期のプロダクト品質指標値を下げる要因となったもので、開発者の心の平穏を脅かすものです。

  1. マルチプラットフォーム対応によるテスト範囲拡大
  2. 導入先サービス特有のテスト不足

これらを順に説明します。

1. マルチプラットフォーム対応によるテスト範囲拡大

エンジンは「PC Web・iOS/Android アプリのマルチプラットフォーム動作をサポートする」ことから、ニコニコ生放送のギフト放送ネタニコニコQなどの共通機構として利用されています。 PC Web 以外のプラットフォームでは、WebView に加え、音声再生や通信などをネイティブで実行しているのが特徴です。

これは即ち、サービス x 共通機構 x 動作環境のサポートが必要になるということを表します。

マルチプラットフォーム対応エンジン

サポート環境の拡大は共通機構であるエンジンの改善や保守の上で大きな課題でした。

エンジンのバージョンを更新するたびに、「全てのサービスをアプリ・Web ブラウザで一通り動作確認する」ことが求められ、リリースには大きな負担が伴うようになりました。 あるときは「iOS の特定バージョンのゲームにのみ動作不良が起きる」不具合が長らく検知できず、hotfix を重ねる事態にまで発展することもありました。

特に Web からはテストしづらい 「iOS/Android 環境のテストとその保全」 が課題となりました。

2. 導入先サービス特有のテスト不足

エンジンは、構成するモジュールの機能ごとに Unit テストが組み込まれています。 一方で、サービスに組み込む単位、ある程度大きな画面演出やゲームなどを実行する場合は、開発者が実際に動作させる必要がありました。

しかし、サービスで利用される画面演出やゲームは、概して「数秒〜数分の時間をかけて」「繰り返し順不同に」API を呼び出すことで実現されるものです。 画面演出を数回行うテストで合格しても、導入先サービスの数千〜数万回繰り返す運用で合格するとは限りません。 これらの組み合わせで起き得る事象は無数にあり、有意なものを厳選して網羅することは困難でした。

一例として、超会議イベントのひとつ超クイズでエンジンが使われたとき、 リリース直前に「クイズの設問やランキングが描画されない」不具合が発生しました。

超クイズ

原因は VRAM 周辺のメモリリークです。

  • HTML Canvas への描画操作が行われなくなる
  • 実行端末のバージョンによって再現性が異なる
  • 実行開始直後に発生せず、特定の操作を契機に発生する

当該イベントの進行に関わるリスクのみならず、過去のコンテンツにも影響を及ぼす問題であり、看過できない問題でした。

これらはエンジンの機能テストでは検出できません。「実際のサービスの画面演出やゲームに応じたビジュアルテスト」 が必要です。

いかに対策したか

品質問題に対して様々なアプローチが行われましたが、ここではエンジン開発チームで検討・提案し導入まで行ったリグレッションテストに絞って対策を紹介します。

効果だけを考えれば、あるサービスの実行環境に特化した E2E テストがあれば品質担保できるはずですが、少し抽象化して 「iOS/Android でも動くビジュアルテスト」 を選定しました。 スマートフォン環境の問題が見逃されやすく、ユーザ影響も大きいことに着目した結果です。

また、単一のシナリオ実行時間が数分から十数分の規模となることが予想されましたが、重要なリリースのタイミングでテストが行えればよいため、許容しました。

利用技術

いわゆる reftest (Reference Test) を使用します。リファレンス HTML とテスト HTML を比較し、期待する結果となることを確認します。 動作環境に依存した HTML Canvas のレンダリング結果も比較できるため、今回の目的となるリグレッションテストとして有用です。

テスト設定やシナリオデータに応じて reftest を行う部位は、内製ツールとして実装することとしました。

Android については Android Emulator を Appium で操作します。 記事執筆時点で iOS は導入計画中ですが、Android と同様に実現できるよう技術を選定しました。

利用技術 採用理由
Node.js/TypeScript コアとなる PuppeteerAppium を利用しやすい。チームで普段使いしている。
Puppeteer ヘッドレスで Google Chrome のエンジン上のコンテンツを起動し、レンダリング結果を reftest のテスト画像とする。
Appium ヘッドレスで Android や iOS アプリを起動し、レンダリング結果を reftest のテスト画像とする。WebdriverIO を利用。
pixelmatch リファレンス HTML とテスト HTML の比較のために使用。ピクセル差分を取れる。
EJS 検証結果を HTML 形式で出力するために利用しているテンプレートエンジン。
Jenkins 特定のタイミングで reftest をキックする。
Android Emulator Androidアプリ実行。AndroidのバージョンはEmulatorに依存している。

構成

サービスごとにテスト設定とシナリオデータ、再現するためのプロダクトコードや画像などリソース一式を用意し、reftest を実行します。

構成

テスト自体のメンテナンスに加えて内製ツールの保守も加わることを考え、可能な限りシンプルに、可搬性のある構造を意識しました。 iOS/Android などの環境依存のものに依存せず、容易に分離できるよう設計しています。

ツール構成1

Android のテストを行う場合は、テスト実行環境に Android Virtual Device(AVD) を作成し、Android Emulator 上でアプリケーションを実行します。

シナリオデータを解釈して実行するシナリオランナーの他に、スクリーンショット撮影タイミングをバインド・受信する ContentOutputReceiver なるサーバを用意しています。

ツール構成2

シナリオデータは、その名の通りテストの期待する結果を再現するためのものです。一般的なゲームではランダムシードやユーザ入力イベントなどを記録する必要があるでしょう。 エンジンにはニコニコ生放送の追っかけ再生やタイムシフト再生で使われる「リプレイデータ」を出力する機能があるため、 これをシナリオデータとして流用することにしました。

また、テスト用の拡張として、必要な場面でスクリーンショットを撮影できるようリプレイデータにコマンドを追加しました。 実際のデータは複雑かつ冗長なため、以下に擬似コードで表します。

[
  { "frame": 100, "event": { "type": "Point Down", "note": "クリック・タップでタイトルへ画面遷移できること" } },
  { "frame": 200, "event": { "type": "Screenshot", "note": "タイトルシーンのキャプチャ" } },
  { "frame": 300, "event": { "type": "Point Down", "note": "クリック・タップでオープニングデモへ画面遷移できること" } },
  { "frame": 400, "event": { "type": "Screenshot", "note": "オープニングデモのキャプチャ" } },
  { "frame": 500, "event": { "type": "Point Down", "note": "クリック・タップでメニューセレクトへ画面遷移できること" } },
  { "frame": 600, "event": { "type": "Screenshot", "note": "メニューセレクトのキャプチャ" } }
]

上記の要領で、必要な導入先サービスのテスト設定を作成し、実際に画面演出やゲームを動作させてシナリオデータを出力します。

検討時点から、シナリオデータを容易に作成し組み込めることには特に配慮しました。 導入先サービス固有のリプレイデータをそのままシナリオとして転用するためです。 これにより、複雑な再現手順をシナリオに落とし込む手間を省いてテストに組み込むことができます。

導入後の運用事例1: メモリリークによる描画問題の検出

前掲の「長時間動作のメモリリークにより描画が行われなくなる」問題に reftest を導入してみました。 reftest は Android/PC Web など環境ごとに以下の HTML を出力します。

reftest NG

図中右列の誤差 2.80000…% がピクセル差分になります。 中央のテスト HTML(出力画像)を見ると、「SCORE:0」などのテキストが描画されていないことが見て取れます。

端末依存で再現が難しい部類の不具合でしたが、このようにピクセル単位での誤差として検出できるようになりました。 これらの成果物は時系列・環境別に記録しており、問題の発生傾向を掴み、プロダクトの健全性を把握することが容易になります。

導入後の運用事例2: Web サイトのサンプルコード

reftest を公開 API のサンプルコードに導入した事例を紹介します。

これまでお話ししてきたエンジンはオープンソースとして Web 上に公開されています。 エンジンの公式サイト では「実際にその場で動かせるサンプルコード」を掲載しており、 ユーザがコードとその評価結果である画面表示を同時に確認できる構成となっています。

Sample code

サンプルコードはエンジンのメジャーバージョンごとに記述されています。 あまりの数により、あるバージョンで描画結果が壊れても検出することが困難な規模となっていました。

これらを reftest 化することで、影響が一目瞭然となりました。 以下はテストの一部ですが、実際にはこの下部にシナリオが並んでいます。

Sample test

複雑度の高いサービスの再現のみならず、シンプルなビジュアルテストを大量に走らせるケースでもメリットを享受できました。 テストの増加に伴いサーバの負荷対策が課題になりますが、並列性のあるシナリオのためスケールアウトは容易であると判断しています。

おわりに

今回は reftest を使ったエンジニア主導によるプロダクト改善アプローチをご紹介しました。

導入の前後で品質指標値を比較し、「いかに目標を達成したか」までご覧頂きたかったのですが、あいにく道半ばの事例紹介と相成りました。 所感をまとめると、内製ツールを開発・運用するコストとリターンの収支はだいぶプラス寄り、コスパの良い対策であったと判断できます。

reftest を導入する上で特に留意した要件は以下の通りです。

  1. テストシナリオを容易に作成・実行できる運用性
    1. 導入先のエンジンのリプレイ性を生かすことでテストシナリオ量産を可能としました。
    2. 仮に「テストシナリオをゼロから自作せねばならない」状況であった場合、採用を見送っていたことでしょう。
  2. PC Web、iOS/Android など様々な環境で実行できる拡張性
    1. 汎用的なテスト実行部分と Emulator など環境依存部分を明確に分離しました。
    2. 今後も必要に応じて環境を追加できる構造としました。
  3. 大量の reftest を実行できるスケーラビリティ
    1. テストの並列性を生かし、シナリオの長さに応じて実行サーバを分割したりスケールアウトできるようにしました。

reftest は「紹介した課題に対する対策手段の1つ」として採用しました。 結果として 「iOS/Android で動くビジュアルテスト」 が現場にもたらされ、適用範囲の広さにかけては白眉のツールが手元に残ることとなりました。

そんな成果を前にして、様々な応用を考えてしまうのは開発者として無理からぬことでしょう。 たとえば HTML Video -> Canvas に再生するだけのテストを作ればライブストリーミングの健全性を可視化できてしまいます。 静止画にとどまらず動画そのものを出力するのはどうでしょう。 せっかくなので使い倒したいところです。

前掲の導入後の運用事例は「せっかくだから」と reftest を導入したことが成果に繋がりました。 今後もこれに続く活用事例を検討していきます。 今後、サービス要求の変化や保守コスト肥大化によりその役目を見直すことになったとしても、「まあそれでも十分に元は取れた」と主張できるはずです。


株式会社ドワンゴでは、教育事業、ニコニコ事業、モバイル事業など様々なサービス、コンテンツを一緒につくるメンバーを募集しています。 ドワンゴに興味がある。または応募しようか迷っている方がいれば、気軽に応募してみてください。