LINEヤフー Tech Blog

LINEヤフー株式会社のサービスを支える、技術・開発文化を発信しています。

ヤフー検索におけるビジュアルリグレッションテストの取り組みと、TypeScript Compiler APIの活用事例

LINEヤフー Advent Calendar 2023の14日目の記事です。

初めまして、ヤフー検索のフロントエンド開発をしている鈴木悠馬です。

今回、ヤフー検索において、ビジュアルリグレッションテスト(以下、VRTest)を導入しました。この記事では、ヤフー検索が直面していた課題と、解決するために導入したシステムの概要をご紹介します。また、今回のVRTestシステム構築で活用した、TypeScript Compiler APIの使用方法についても詳しく説明します。

VRTestを必要としたヤフー検索の課題

まず、なぜVRTestを必要としたのか、ヤフー検索が抱えていた課題についてお伝えします。

ヤフー検索というプロダクトは、入力された検索キーワードによって検索結果画面が変化します。同じキーワードでも時間の経過によって内容は変化します。また人や場所などによって最適化されるケースもあり、表示されるパターンは実質的に無限です。

たとえば、箱根駅伝の開催される時期が近ければ、ヤフー検索で作成した箱根駅伝の詳細情報を表示するダイレクト検索と呼ばれる検索結果が出ます。しかし開催時期が近くなければ出ません。

ダイレクト検索が出ている箱根駅伝の検索結果の画像

例として、「鬼滅の刃」という検索キーワードの結果と、「スプラトゥーン」という検索キーワードの結果を比べてみると、URLのパスとしては/searchで同じですが、検索キーワードとなるURLパラメータ?p=が違うだけで、実際にユーザーが見る検索結果はまったく違うダイレクト検索や、サイトの一覧(以下、オーガニック検索)が表示されます。

かつ、このURLパラメータはユーザーの検索キーワードによって無限に変わる可能性があるという特性を持っています。

鬼滅の刃の検索結果の画像

スプラトゥーンの検索結果の画像

このプロダクトの特性上、パフォーマンスの向上や汎用(はんよう)コンポーネントの修正など、検索機能全体に影響を与える変更を行うと、すでにユーザーに提供されているさまざまな検索結果において、その変更が問題を引き起こしていないかを確認することが非常に困難になります。

そこでこの課題を、現実的なコストで可能な限り解決する手段として、今回のVRTestを導入しました。

VRTestの概要

ここからは、VRTestの概要についてお伝えします。

ヤフー検索の結果は、ダイレクト検索、オーガニック検索などの要素とその組み合わせパターン(以下、検索構成要素)により構成されています。前述したように、時間の経過や検索キーワードによって表示される内容が変わることがあります。そのため全種類の検索構成要素をVRTestでチェックすることは、現実的に困難です。

この前提があるため、今回のVRTestでは、すべての検索構成要素を網羅する完璧なテストを作るのではなく、現実的なコストで、できる限り多くの検索構成要素を本番環境と検証環境間で比較・確認し、変更による不具合が本番で発生するリスクを下げることを目的に開発しました。

また可能であれば、VRTestでカバーしている検索構成要素を具体的に明示することが望ましいのですが、検索クエリやユーザーの地理的位置による結果の違いによって、検索結果が多くのパターンに分かれるため、最初からカバー範囲を明確にすることは難しいです。

この問題に対処するために、VRTestを実行するたびに確認できた、または確認できなかった検索構成要素を観測・集計し、その情報を最後にまとめて出力することで、どの検索構成要素が問題なく機能しているか、またどれがまだ確認できておらず不具合のリスクを抱えているかを可視化しています。

具体的なシステムのインターフェースについては、以下のような構成としました。

  • 実行のトリガー: GitHubのPRにcheck-two-env-diffというLabelをつける
  • 実行環境: 社内のCI上で実行
  • 結果の出力: Slackチャンネルに以下の形式で出力
    1. 画像差分などの結果: テストを回した検索キーワードごとにスレッドを作り画像差分や確認したURLなどを投稿
    2. チェックできた検索構成要素の一覧/詳細情報: 1.の結果を出力し終わった後にListやJson形式でSlackチャンネルにまとめて投稿

VRTestのシステム構成

システム概要画像

次にVRTestのシステム構成をお伝えします。

  1. 最初にヤフー検索に存在する検索構成要素を把握する必要がありました。しかし検索構成要素はパターンが大量にあるため、すべてを把握するのは容易ではありません。検索構成要素を把握するための手段として、TypeScript Compiler APIを使いました。
    具体的にはヤフー検索のソースコードに対して、TypeScript Compiler APIで解析をかけてリスト化し、重要な検索構成要素の情報を得ました。こちらについては、後ほどの章で詳しくお伝えします。

  2. 1のセクションで生成した検索構成要素のリストを元に、それぞれの検索構成要素に関連するキーワードをリスト化しました。現時点では人手に頼っている部分もあるため、こちらについては今後、VRTestの使用状況などに応じて、自動化していければと考えています。

  3. 実際に画像差分やエラーを検知する処理については、アクセス用のブラウザ環境の設定やスクリーンショットなどの一般的な処理を行うために、puppeteerというライブラリを使用しました。この処理の詳細については以下に記載しています。

    • まず、アクセスした検索結果画面のスクリーンショットを撮る処理です。 こちらはpuppeteerのスクリーンショット機能を使い、以下のような簡単なコードでテスト環境、本番環境それぞれでスクリーンショットを撮れました。

        const captureScreenShot = async (page) => {
          const b64ScreenShot = await page.screenshot({
            enconding: "base64",
            fullPage: true,
          });
          const bufferScreenshot = Buffer.from(b64ScreenShot, "base64");
          return bufferScreenshot;
        };
    • 次に、画像の差分を取る処理の部分です。こちらについてはreg-viz/img-diff-jsを使用しました。以下のようなコードを書くことで、簡単に下の画像のような画像差分を取れました。

        const getImgDiff = async (testEnvFile, expectedFile) => {
          const ACTUAL_FILE_NAME = "./actual.png";
          const EXPECTED_FILE_NAME = "./expected.png";
          fs.writeFileSync(ACTUAL_FILE_NAME, testEnvFile);
          fs.writeFileSync(EXPECTED_FILE_NAME, expectedFile);
          const { imagesAreSame } = await imgDiff({
            actualFilename: ACTUAL_FILE_NAME,
            expectedFilename: EXPECTED_FILE_NAME,
            diffFilename: "./diff.png",
          });
          return { imagesAreSame, imgDiffPng: fs.readFileSync("./diff.png") };
        };

      画像差分検知の結果画像

  4. 最後に画像差分の検知結果について結果を出力する処理です。こちらについては以下のようなSlack APIを用いたベーシックな関数を作ってSlackのVRTest用のチャンネルに通知するようにしました。
     const uploadFileToSlack = async (file, fileName, threadTs = undefined) => {
       try {
         await lib.slack.files.upload({
           title: fileName,
           channels: CONFIG.slackChannel,
           file: file,
           thread_ts: threadTs,
         });
       } catch (e) {
         console.log(`SEND TO SLACK ERROR: ${e}`);
       }
     };
     const sendMessageToSlack = async (message, threadTs = undefined) => {
       try {
         const initialPost = await lib.slack.chat.postMessage({
           channel: CONFIG.slackChannel,
           text: message,
           thread_ts: threadTs,
         });
         return initialPost.ts;
       } catch (e) {
         console.log(`SEND TO SLACK ERROR: ${e}`);
       }
     };

以上がVRTシステムのシステム構成と概要でした。

TypeScript Compiler APIの活用と所感について

次に今回活用したTypeScript Compiler APIについて述べていきます。このVRTestのシステムにおいて、一般的なVRTestとの違いは、TypeScript Compiler APIを活用し、運用している点にあると考えています。

こちらは章を分けて、以下に詳細を記載します。

TypeScript Compiler APIの活用方法

前述のシステムの構成に関する章で述べましたが、TS Compiler APIは、実際にヤフー検索を実行しているシステムのコードから、検索構成要素の一覧を取得するために使用しました。

処理の流れとしては、以下のような形です。

  1. ヤフー検索のコードからfsライブラリで検索構成要素を取得するのに必要な部分を読み込む
  2. TypeScript Compiler APIを使って重要な検索構成要素のリストを取得する
  3. fsライブラリで検索構成要素のリストをファイルへ書き出す

以下は、ステップ2のコードについてイメージを詳しく書いたものです。

import * as ts from "typescript";
const answerList: string[] = [];
const program = ts.createProgram(
  "fsで読み込んだファイル類",
  "fsで読み込んだ参照したいtsconfig.jsonファイル"
);
const visit = (node: ts.Node) => {
  if ("検索構成要素がある部分の条件(例: `ts.isIfStatement(node)`)") {
    answerList.push(`"${node.expression.text}"`);
  }
  node.forEachChild((childNode) => {
    visit(childNode);
  });
};
for (const sourceFile of program.getSourceFiles()) {
  if (sourceFile.fileName.substring(-5) === ".d.ts") continue;
  ts.forEachChild(sourceFile, (node) => {
    visit(node);
  });
}

少ないコード量で必要な機能を満たせたのに加え、TypeScript AST Viewerという便利なサービスを活用しながら書くことで、諸々の確認を合わせても一日もかからずに書けました。(慣れたら、さらに早く書けると思います) あまり時間をかけずに完成できたので、開発コスト的な面でも悪くない選択肢だったと思います。

加えて、運用面についても述べていきます。前の章でも述べましたが、検索構成要素は、随時新しい種類のものが追加されていきます。そのような特性上、VRTestでも継続的に検索構成要素のリストを更新していくモチベーションが生まれます。

そのため、開発した後もこのスクリプトを実行し、定期的に新しく追加された構成要素を一覧のリストに追加するようにしました。

このような継続的に使用されるスクリプトの場合、以下に記載したメリットにより、比較的手軽かつすぐに思いつく「VSCodeなどで置換を繰り返すことでリストを作る」方法や、「特定のキーワード(JSのcase構文など)を元に、コードを純粋な文字列として扱いパースしリストを生成する」方法よりも優れていると考えました。

  • コードを構造的に処理しているため、後から読んだ時にスクリプトの意図がわかりやすく変更しやすい
  • 以下2つの理由により、ある程度の期間運用するようなシステムでも使用できる
    • スクリプトとしてCI上などでも動かせる
    • パース対象のコードに多少の変更があってもスクリプトが壊れにくい

TypeScript Compiler APIを使ってみての所感

TypeScript Compiler APIを実際に業務に導入してみて、プロダクトのコード解析を効率的に行えることを確認しました。これは非常に便利なツールで、作業効率を向上させるとともに、慣れてしまえばさほど時間がかからず、比較的信頼性の高いスクリプトを素早く作成できます。そのため、運用にも役立つと感じています。

今後も必要に応じてツールとしてTypeScript Compiler APIを活用していきたいと思います。

まとめ

以下はこの記事に関するまとめです。

  • ヤフー検索における、「検索全体に影響が及ぶような変更の動作確認が難しい」という課題に対して、VRTestとTypeScript Compiler APIによるコード解析を組み合わせて、変更の影響を可能な限り可視化し、不具合混入のリスクを低減できたと思います。
  • TypeScript Compiler APIは、なんとなくとっつきにくいイメージがあるかもしれません。しかし、慣れれば短い時間でTypeScriptのコードを解析できて、ツールとして純粋に便利です。
  • TypeScript Compiler APIは、「スクリプトとして実行可能かつ比較的丈夫なスクリプトを書ける」という特性を持っているため、ある程度の期間運用するシステムにおいても、安定して活用・運用できそうです。

VRTestシステム全体を振り返ると、「検索全体に影響を及ぼすような変更を加えた際、その変更が現在ユーザーに提供される多様な検索結果で不具合を起こしていないか確認するのが難しい」という検索の課題を完全に解決する銀の弾丸とはなり得ませんが、この課題に対して、リスクの低減/可視化を可能にし、かつ変更に対する動作確認のコストを削減できる堅実で便利なシステムができたのではないかと思います。

以上で、この記事は終了です。
最後まで読んでいただきありがとうございました。