Playwright+MSW でのリグレッションテスト事始

ogp

はじめに

enechainでフロントエンドエンジニアをしている@Shunya078です!

自分の所属するGXデスクでは『日本気候取引所 - Japan Climate Exchange』(以下JCEX)のサービス開発を行っており、その中でReactを使用したフロントエンドの開発を担当しています。

リグレッションテストは運用を考えると、設計から導入した後、どう管理していくかまで検討する点が多く存在します。 JCEXは去年の年末にリリースされたばかりのサービスで、まだブラウザまで含めたリグレッション相当になるテストレイヤーが導入できておらず、存在しませんでした。

今回は新たに自チームに導入した、コンポーネント同士のリグレッションテストとしてPlaywrightとMSWを用いて行うブラウザテストの方法について、紹介したいと思います。

背景

顧客影響が存在するような規模のリリースでは、社内で横断的に動いているQAチームにQAを依頼してリリース判断を行ってもらっています。 その中で実施しているリグレッションテストは現在手動で行っているので、これを自動化することによって、QAチームの負担を削減することを考えました。

初期リリースを終え、運用フェーズに入ったプロダクトでは、経年で動作確認項目は増えていきます。網羅的にテストケースを消化することはその分メンバーのリソースを消費することになります。 そのため、リリースまで含めた開発フローの中にリリース判断の基準ともなる、リグレッションテストを導入することとなりました。

やりたいこと

JCEXは認証が必須のサービスです。認証にはAuth0を使用しているので、テスト時にAuth0の認証を突破する必要があります。

また、今回はリリース可否の判断要素として扱いたいので、できるだけ本番環境に近い形でのテストをする必要があります。JCEXではViteを採用しているので、ビルドの成果物を配信している環境でテストすることを考えます。

加えて、推奨ブラウザとしてサポートしているのが複数存在するので、それぞれのブラウザでの動作も確認したいと考えていました。

これらを踏まえて、使用ライブラリとしてはCypressやPuppeteerなど他ライブラリも存在しますが、社内で過去導入しようとしていたPlaywrightを使用することに決定しました。また直近ではトレンドとして伸びていることも考慮に入れました。

e2eライブラリのnpm-trend ref: https://npmtrends.com/cypress-vs-playwright-vs-puppeteer-vs-selenium-webdriver-vs-testcafe

Why MSW?

ページ単位でのリグレッションテストを行う上ではAPIとの疎通を考える必要があります。実際の開発環境に繋いで進めることも選択肢にありましたが、特定条件以外の外部からのリクエストを遮断しているので、テストサーバー構築時に模倣することが難しく断念しました。

Dockerを用いてバックエンドのアプリケーションやDBを立ち上げてAPIの環境を再現することも考慮しましたが、実行時間がボトルネックになってしまうため、今回はMSW (Mock Service Worker)を導入し、APIの動作はモックで保証することにしました。

MSWはService Workerを利用して、ネットワークリクエストをインターセプトします。特定のAPIリクエストをインターセプトすることによって、想定されるユースケースの表現を可能にしました。

導入手順

Playwrightの導入手順に合わせて、必要ファイルを導入していきます。

https://playwright.dev/docs/intro

// パッケージマネージャーに合わせて実行コマンドは変更
$ pnpm create playwright

... 必要な要件に合わせて応答する

これによって、サンプルの実装となるtests/と、設定ファイルとなるplaywright.config.tsが追加されます。

次に、MSWを使用するためにplaywright-mswを追加で導入します。 インターセプトするためのextends方法やtestファイルへの追記方法は以下READMEにまとまっているので、今回は割愛します。

https://github.com/valendres/playwright-msw/blob/main/packages/playwright-msw/README.md

$ pnpm install playwright-msw --save-dev

JCEXではやりたいことに記載した通り、Auth0を用いた認証機能があるので、開発環境をターゲットにしたクライアント情報などをenv経由で参照するようにしました。そのため、package.jsonにテスト用のbuild scriptを追加しています。

{
  ...
  "scripts": {
    "dev": "vite",
    "build": "tsc --noEmit && vite build",
    "preview": "vite preview",
    "e2e": "playwright test",
+   "build-e2e": "vite build --mode test"
  }
}

MSWを導入しているので、ユーザー情報などをハンドラーに用意し、テストの場合は認証をスルーするような実装も可能ですが、以下2点を考慮して上記のアプローチを取ることにしました。

  1. テストのためだけのコードを本番環境に残さない
  2. なるべく実挙動に近い形を再現する

続いて、認証を通すためのテストケースは、全テストケースの前に実行する必要があります。 Best Practicesに紹介されているように、beforeEachなどのHooksを使用して対応可能ですが、ブラウザ単位で事前に実行しておきたいので、今回はsetup projectを追加し、各ブラウザに認証情報の入ったストレージの状態を渡すようにしました。

// playwright.config.ts
export default defineConfig({
  // 差分省略
  projects: [
    { name: 'setup', testMatch: /.*\.setup\.ts/ },
    {
      name: 'chromium',
      use: {
        ...devices['Desktop Chrome'],
        storageState: './playwright/.auth/user.json',
      },
      dependencies: ['setup'],
    },
  ],
})

認証に必要な秘匿情報であるメールアドレスとパスワードは、環境変数から取得するようにしました。具体的には、tests/auth.setup.tsで使用しています。

// tests/auth.setup.ts
import { test } from '@playwright/test'

import process from 'node:process'

const authFile = './playwright/.auth/user.json'

test('authenticate', async ({ page }) => {
  await page.goto(`http://localhost:3000`)

  await page.waitForURL(/login*/)

  await page.getByLabel('メールアドレス').fill(process?.env?.DEV_EMAIL ?? '')
  await page.getByLabel('パスワード').fill(process?.env?.DEV_PASS ?? '')
  await page.getByRole('button', { name: '続ける' }).click()

  await page.waitForURL(/3000/)

  await page.context().storageState({ path: authFile })
})

※ 環境変数の参照にはドキュメントに則る形で、dotenvを使用しています。playwright/envsディレクトリを用意して、CI上ではGitHub ActionsのSecretを参照しています。

これによってPlaywright実行環境下で、全てのテストケースの実行前に認証を通すことが可能になりました。

また、Playwrightはplaywright.config.tswebServerを用いることによって、playwright test実行時に任意のサーバーを立ち上げることが可能です。今回は開発モードではなく、build後の成果物をターゲットにしてテストを実行したいので、vite previewでビルド成果物を配信するようにしています。

https://playwright.dev/docs/test-webserver

// playwright.config.ts
export default defineConfig({
  // 差分省略
  webServer: {
    command: `pnpm build-e2e && pnpm preview`,
    url: `http://localhost:3000`,
    reuseExistingServer: process.env.CI == null,
  },
})

これにて、Playwrightを使用してテストケースを実行できるようになりました🎉

具体的なテストケースなどはVSCodeの拡張を使用して、作成するようにしています。詳細は割愛しますが、ブラウザレコーディングからテストコードを作成したり、テストしたい要素のセレクタを確認可能です。

https://playwright.dev/docs/codegen

詰まったこと

defaultのtimeout設定時間が短い

Playwrightのexpect timeoutはデフォルトで5000msが設定されています。

Timeout Default Description
Test timeout 30000 ms Timeout for each test, includes test, hooks and fixtures:config = { timeout: 60000 }
Expect timeout 5000 ms Timeout for each assertion:config = { expect: { timeout: 10000 } }

ref: https://playwright.dev/docs/test-timeouts

認証後のリダイレクトを待っているタイミングで5000msが過ぎる時もあり、テストがフレーキーになってしまっていたので、数回計測してplaywright.config.tsにて設定を変更しました。

// playwright.config.ts
export default defineConfig({
  // 差分省略
  expect: {
+   timeout: 15 * 1000,
  },
})

CIで落ちた時の検証方法がわからない

Playwrightにはreport機能があり、これを使用することによって、失敗した際の挙動をログと動画で確認できます。

https://playwright.dev/docs/ci-intro

// playwright.config.ts
export default defineConfig({
  // 差分省略
  retries: process.env.CI != null ? 2 : 0,
  use: {
    trace: 'on-first-retry',
    video: 'on-first-retry',
  },
})

retry回数を2回に設定し、失敗した際はactions/upload-artifactを使用してzipファイルを出力するようにしています。

# テストを実行するActionsファイル
- uses: actions/upload-artifact@v4
  if: failure()
  with:
    name: playwright-report
    path: frontend/apps/hoge/playwright-report/
    retention-days: 1

失敗したActions

zipファイルをローカルマシンにダウンロードし、解凍後に以下を実行すると、実行時の状態を確認できます。

$ npx playwright show-report playwright-report # playwright-report はダウンロードしたzipファイルの命名

認証後のストレージの状態が入ってこない

setup projectにおいて作成するstorageStateにはファイルが存在している必要があります。

しかし、ストレージの情報は認証Tokenも含まれている&実行時に毎回書き変わってしまうので、GitHubに差分として追跡してほしくない情報となります。build-e2eの時にsampleとして置いているjsonをコピーする方法を行っていましたが、「1秒でも早くする!」の精神で、今は./playwright/.auth/user.jsonを一度{}であげて、以下のgit commandで追跡しないようにしています。

$ git update-index --assume-unchanged frontend/apps/hoge/playwright/.auth/user.json

今後の展望

今後の展望として、考えている課題は大きく2つあります。

1つはまだ用意しているテストケースが多くないので、worker数なども特に気にせず運用していますが、テストケースの肥大化によって実行時間の遅延が発生することです。実行時間を短くすることは、PRごとにテストをすることによる開発効率の向上、リリースサイクルの短縮などに繋がると考えています。

2つ目はMSWで用意しているmockデータの用意です。現在JCEXではgRPCのためにprotobufとconnectを使用していますが、proto定義に基づいたmockデータの自動生成を行えていないので、開発環境のデータを手作業で更新しています。これを自動作成することによって、より網羅的なテストケースの用意が可能になると考えています。

おわりに

本記事では、JCEXに導入した、PlaywrightとMSWを使用したリグレッションテストの説明と具体的な背景、運用方法を紹介しました。

本記事が、リグレッションテスト環境の準備に困っている方に少しでも助けになれば幸いです。

enechainでは、共にプロダクトを開発していく仲間を募集しています。要項は以下からご確認ください!

herp.careers