🤖

STORESにE2Eテストを導入した話

2021/12/14に公開

これ何

これは hey Advent Calendar 14日目の記事です。

hey株式会社で STORES のフロントエンドエンジニアをしているものです。社内ではうめきちやうめくんと呼ばれることが多いです。普段は STORES というネットショップ作成サービスのダッシュボード上に弊社POSレジアプリをサポートする機能を組み込んでいます。今回はそのダッシュボードにE2Eテストを導入した話を紹介していきます。

解決したい問題

フロントチームで、本番環境で発生した障害を減らす、または発生したことにすぐに気づきサービスのダウンタイムを少なくするにはどうするべきかを考える機会がありました。

リリース前に機能を実装したチームで起こりうるテストケースを並べて機能が想定した通りに動作するかを確認するといったQAをしているのですが、どうしても手動で確認漏れが起こったり、軽微な修正を加えた際に、特定のブラウザで予期せぬページで発生したケースがあります。ユニットテストで気づけない部分は改修するたびに、再度手動で確認するのも時間と手間がかかってしまいました。
ダッシュボードではネットショップ全体の共通コンポーネントライブラリを使っていることから、変更が発生したときの心理的負担が高まってしまう問題もあります。

そのような背景もあり、主に解決したい問題に下記2つが挙げられ、解決を図りたくE2Eテストを導入することになりました。

  • ユーザーストーリーに沿うような形で動作し、アプリケーションが想定どおりに動いているかの確認を自動で確認したい。
  • ユニットテストでは気づけない問題の検知がしたい。(ページがブラウザでしっかりレンダリングされているかなど)

システム構成

システム構成としては、下になります。

テストの実行タイミングとして、デプロイを終えた後で1回実行があると、深夜の定期実行が1回あります。どちらの実行も、E2Eライブラリには Playwright を、CIにはGithub Actionsを採用しています。

どうしてPlaywrightなのか

  • テスト実行ブラウザが豊富であった。
    • STORES 開発チームでQAを行うときのブラウザには Google Chrome(Chronium) / Safari(WebKit) / Firefox があります。過去の障害で、Google Chromeでは問題なくページが表示されましたが、WebKitでページが表示されずエラーになる問題が発覚したことがありました。普段の開発でも特にSafariの動作確認に力を入れることが多いため、他のE2Eテストライブラリと比べて、PlaywrightでWebKitでのheadlessブラウザでテストを実行できることが魅力的でした。
  • フロントチームの技術スタックにマッチしていた。
    • hey product blogの記事のプロダクトに途中からTypeScriptを導入した話 で紹介したように、 STORES のフロントチームでは、開発言語にTypeScriptを、ユニットテストにjestを採用しています。PlaywrightはTypeScriptフレンドリーで、提供しているAPIの名前と実行内容が非常にjestに似ているため、チーム展開するときのラーニングコストが低いのではないとかと思いました。
  • コードの自動生成があることが魅力的だった。
    • 幸いなことに Playwrightはとてもドキュメントが充実しています。ドキュメントを読んだだけで、どのようにテストを書いていくべきかある程度把握できます。しかし、筆者含めフロントチームでPlaywrightを使うことが初めてのメンバーが多いです。どう表現するべきかわからないケースがあるため、Playwrightの自動生成でわからない箇所の手助けになると考えました。雛形としても役目を果たすと考えると、テストを書く時間の短縮に繋がるのではないかと考えました。
  • 主流なフレームワークのPuppeteerよりも使いやすいAPIがそろっていた。
    • ログインセッションの保持およびそれの使い回し、実機とブラウザの組み合わせをグローバル設定に書ける、またブラウザの生成と破棄をする必要がなく、コードを書く量を大幅に削減できるのではないかと考えました。
    • 補足までに、Puppeteerと比較表を参考までにこちらに記載しています。
      • 特徴 Playwright Puppeteer 感想
        使用できるブラウザ Chromium / WebKit / Firefox Chronium / Firefox Playwrightだと WebKit でのテストが行える。
        使用できる端末 iPhone12まで / GalaxyやNexusやPixelなどのAndroid端末(デバイス一覧) iPhone11まで / NexusやPixelなどのAndroid端末 (デバイス一覧) 両方充実していて、特に差異がなかった。
        テストするブラウザと端末の用意 実機とブラウザの組み合わせをグローバル設定ファイルに書くだけとなる 設定ファイルがないので、実機とブラウザの組み合わせの一覧を別途用意しないといけない Playwrightだと組み合わせを保持できるAPIが用意されているので、楽に感じる。
        TypeScriptで書けるか yes yes 両方TypeScriptで書ける。
        API DOM検索 / スクリーンショット / ダウンロードなど DOM検索 / スクリーンショット / ダウンロードなど APIの差異は特にないと感じた。
        テストランナー jestライクなのでほぼラーニングコストはなし jestなのでラーニングコストなし ただPuppeteerだとページの初期化と破棄のコードを書かないといけないで、それが手間だと感じた。
        設定ファイル playwright.config.ts jest.config.ts Puppeteerだと、事前にpresetを読み込んだ専用のjest.configを用意するので、ユニットテスト用のjest.configを用意しないといけないのがややこしくなる。
        DOM検索 特にテキスト検索について、$('text=hogehoge') でうまくいくため、手間がない。 DOMにあるテキストでの検索が少し扱いに手間がかかるx(//div(contains[text(), 'hogehoge') xの返り値は配列になるので、要素を取得するのに returned[0]とかしないといけない。
        ログインセッションの保持 yes (そのためのAPIが用意されている) yes(可能だけど、nodeのfile APIを使わないといけない) PuppeteerだとAPIが用意されていないので、自前で処理を用意しないといけない。

どうして Github Actions なのか

  • 既存で使っているCIの実行時間を伸ばし、ジョブを詰まらせたくないため。
    • 弊社ではCIにCircleCIを採用しています。多くのメンバーと共に働けることはありがたいことですが、その分普段の開発でCircleCIのジョブが多く積まれがちです。これから導入するE2Eテストも実行時間が長くなると予測されるため、既存の開発業務に加えてE2EテストもCircleCIに導入すると、開発業務やリリース作業にCIのリソースが割けなくなり支障が出てしまう恐れがありました。そのため、CircleCIとは別のCIを使用することによって、リリースのためのリソースを確保できると考えました。
  • 指定したタイミングでE2Eテストを実行できるため。
    • Github Actionsには、指定時間にジョブを走らせるScheduled Eventや、ある特定のイベントにフックしてジョブを実行できるRepository Dispatchという機能があります。要件には夜間バッチで何かシステムに問題が起こっていないかを確認したかったのと、デプロイ後で何か問題があるコードが動いていないかを確認したいことがありました。Scheduled Eventで夜中の指定した時間に実行できるのと、デプロイが終わったあとRepository DispatchのAPIを叩き、それをトリガーとして使えば、要件を満たせると判断したため Github Actionsを採用しました。
  • まずはスモールスタートから始めたかった。
    • E2Eテストの運用を考えると、自動化や速度向上、テストデータの用意や破棄、テストケースの準備、テストの実行環境など考慮する点がどうしても多くなります。これらすべてについての問題を解決した環境を構築するにはコストが高くなります。そのため、まずはスモールスタートで本番環境でページが正常にレンダリングされているかを確認するテストを用意することをゴールに決めました。
    • Github Actions を採用した弊害として、ページへアクセスするのに使用するIPを固定にできないことから、社内開発環境に向けて実行できません。それにより、データ変更が発生するサービスの核になる機能の確認が取れていません。社内開発環境との接続は来年の目標にしており、今後接続することを予定しています。

実行結果

実行が完了したときにはこのようにslackに通知が来るようにしています。

気をつけたこと

DOM特定にdata-testidを使っていくこと

E2Eテストの運用で直面する問題に、テストが壊れやすいことが起因する、高い維持コストがあります。原因として、E2Eテストの特性として、DOMを特定し何かアクションをかけるため、UIやDOMの変更に対して弱いことが挙げられます。 STORES でも日々機能を改修をしていくため、E2Eテストはその影響を受けて壊れてしまうことが予想されました。

そこで、UIの変更に強くするため、DOMの特定にはCSSやテキストを使わず、data-testid という属性を使ってDOMの特定をしています。Reactのユニットテスト導入でよく採用されるtesting-libraryなどのテストに関するライブラリのドキュメントでも、DOMの特定に data-testid を使用する例があります。Playwrightも例に漏れず、data-testid を使ってDOMを特定できるAPIが用意されているため、それでDOMを特定することにしました。

SaveButton.vue
 <template>
   <button
     data-testid="save_button"
     @click="save"
   />
 </template>
button.e2e.test.ts
await page.click('data-testid=save_button');
await expect(page.waitForSelector('data-testid=save_button')).resolves.toBeTruthy();

チーム展開のためにガイドラインを作成したこと

STORES のフロントチームではあたらしい技術を導入する際に、みんなで認識を合わせたり新しく入社したメンバーがキャッチアップをしやすくするために、社内ドキュメントにガイドラインというものを用意しています。上記のDOM特定に data-testid を使っていくことや何をテストするのかなど、テストを書くのに読めば迷うことがなくなるようにする社内ドキュメントとなります。

E2Eテストは、導入して終わり、ではなくカバレッジをあげ普段からアプリケーションが問題なく機能しているかを確認する必要があります。筆者一人だけでは到底カバレッジをあげることは不可能であるため、チームメンバーの力を借り、みんなでカバレッジをあげる環境を用意するのが目標でした。

実際のドキュメントの一部がこちらです。

こちらの社内ドキュメントはみんな一人一人に編集権限が付与されているため、ここを詳しく記載すると質が良いものになると感じたことを一人一人書き合っていきます。

大変だったこと

複数アカウントでのログインを可能にしたこと

今回ユーザーが自分自身のネットショップを自由にカスタマイズできるダッシュボードに向けて、E2Eテストを導入しています。ダッシュボードには閲覧制限がかけられているページが多くあります。最近だと配送日時指定機能がリリースされましたが、こちらのページはユーザーがスタンダードプランでないとアクセスできません。そうなったときにE2Eテストの内でダッシュボードにアクセスするアカウントはスタンダートプランの権限を持つ必要があります。

まずはページがレンダリングされていることがしっかり担保されている状態にしていきたいと考えているので、スタンダードプランになっているアカウントを用意しました。他にも特定の権限が付与されたアカウントではないとページにアクセスできないため、複数のテストアカウントを用意することになりました。

そうなると、Playwrightの中で複数アカウントでのログインを可能しないといけない問題に直面したため、それをどうしたかのコードを公開します。

playwright.config.ts
const config: PlaywrightTestConfig = {
  globalSetup: './lifecycle/global_setup/index',
}

export default config
lifecycle/global_setup/index.ts
import { multipleAccountsLogin } from './session';

const globalSetup = async (): Promise<void> => {
  await multipleAccountsLogin();
};

export default globalSetup;
lifecycle/global_setup/session.ts
import { chromium, firefox, webkit } from '@playwright/test';

import { SIGNIN_URL, TESTING_ACCOUNTS } from '../../constant';

export const multipleAccountsLogin = async (): Promise<void> => {
  for (const b of [chromium, webkit, firefox]) {
    const browser = await b.launch();

    for (const account of TESTING_ACCOUNTS) {
      const { id, email, password } = account;

      const context = await browser.newContext(); // ここで新しいcontextを生成する

      const page = await context.newPage();
      await page.goto(SIGNIN_URL);

      await page.fill('data-testid=email-input', email || '');
      await page.fill('data-testid=password-input', password || '');

      await page.click('data-testid=login-button');
      await page.waitForNavigation();

      await page.context().storageState({ path: `./tmp/sessions/${id}-account.json` });
    }

    await browser.close();
  }
};

幸い Playwrightにはログインセッションを一時保存し、テストの限られたスコープの中でそのセッションを適用させることができます。テスト時の中身で、 test.describe の中で test.use を使うことでテスト時のブラウザで適用されるアカウントのログインセッションを選択できます。

tests/items/bulk_inventory.e2e.test.ts
import { expect, test } from '@playwright/test';

import { ITEMS_BULK_INVENTORY_PATH, ITEMS_URL, LOGIN_IDP_URL } from '../../constant';

test.describe('items/bulk_inventory', () => {
  test.describe('スタンダードのアカウントでログインしているとき', () => {
    test.use({ storageState: './tmp/sessions/ec-standard-account.json' });

    test('アイテム在庫一括更新ページがレンダリングされること', async ({ page }) => {
      await page.goto(ITEMS_BULK_INVENTORY_PATH);

      await expect(page.waitForSelector('data-testid=bulk_inventory_page')).resolves.toBeTruthy();

      await expect(page.url().endsWith(ITEMS_BULK_INVENTORY_PATH)).toBe(true);
    });
  });

  test.describe('フリーのアカウントでログインしているとき', () => {
    test.use({ storageState: './tmp/sessions/ec-free-account.json' });

    test('アイテム一覧ページがレンダリングされること', async ({ page }) => {
      await page.goto(ITEMS_BULK_INVENTORY_PATH);

      await expect(page).toHaveURL(ITEMS_URL);
    });
  });

  test.describe('ログインしていないとき', () => {
    test('IDPのページへリダイレクトされること', async ({ page }) => {
      await page.goto(ITEMS_BULK_INVENTORY_PATH);

      await page.waitForSelector('data-testid=login-button');

      expect(page.url()).toContain(LOGIN_IDP_URL);
    });
  });
});

他チームへの連携

他に大変だったことに他チームへの連携もありました。End-to-Endなので、フロントチームだけで導入を終えられるものだけではなく、プロダクト開発に関わっている多くの人を巻き込む必要があります。

他チームへの連携で、具体的に行ったのは以下になります。

  • STORES はheyプロダクトの1つに過ぎなく、heyは元々複数の会社が1つになった会社です。1つのアカウントでさまざまなサービスを使えるように、認証については各サービスが共通して使う共有の認証基盤があります。ログイン画面がその共有基盤の管轄にあるため、そちらのチームにログインページに data-testid を入れる対応をお願いしました。
  • これからページのレンダリング以外のテストでカバレッジをあげる際に、各テスト実行前にデータを初期化できるように、バックエンドチームにデータケアの依頼などもお願いする予定です。
  • 来年対応する社内検証環境へのアクセスは、SREチームとの連携が必要になります。

Playwrightで残念だったこと

上記でPlaywrightを採用した理由を記載しましたが、実際に導入してみて、事前に知っておきたかった情報も記載しておきます。

テスト内の動作の録画がwebm拡張子のファイルになってしまう

Playwrightの中に、テスト内でのブラウザでの動作を録画し、それをビデオファイルに録画している機能があります。しかし、実際に使ってみると、mp4 拡張子ではなく、webm 拡張子で保存されていました。mp4 の拡張子でビデオファイルを扱いたい人は変換処理が必要になります。

ちなみにこの件についてのGithubのissueを見たところ、Playwrightとしては mp4 のビデオを作成する予定がないようなので、テスト内容を録画するときは注意が必要です。

導入してみて

現在まだ本番環境で数ページのレンダリングがしっかりできているかの確認しかできていない状況ですが、デプロイ後や夜間で定期的にE2Eテストを実行し、問題が発生していないことを随時知れているため、今までよりも安心しながら開発できています。 STORES サービスのフロントエンド開発では、heyの各プロダクトが使う共通のコンポーネントライブラリを使用しているため、変更のたびにアプリケーションに影響を与えていないことを知れるのは精神的に安心した状態で開発を行えます。

とはいえ、Github Actionsを採用した背景に記載した通り、社内用検証環境に接続することができていないため、サービスの核となる機能の確認はできておりません。今後はしっかり確認できるようにして、 STORES をより障害の発生しないアプリケーションにしていきたいと考えています。

まとめ

今回 STORES プロダクトにE2Eテストを導入した話を紹介しました。導入する目的やまた使用する技術、導入するまでに考慮したことや課題などについての詳細を少しでも知ってもらえたら嬉しいです。

この記事は hey アドベントカレンダー 2021 の 14日目の記事でした。E2Eテストを導入しようとしている方の参考になれば幸いです。ここまでのご精読ありがとうございました。

Discussion