Playwrightを使ったE2Eテストの導入Tips

PlaywrightとはE2Eテストを導入できるライブラリです。今回はPlaywrightの導入に関する所感とTipsについて書きます。

E2Eを導入する目的と方針

E2Eを導入する目的として「安定して継続的なデリバリーを行う」ことが挙げられます。具体的には以下のようなメリットが考えられます。

  • 障害発生率の低下
  • 動作確認の時間的工数・精神的工数の削減
  • デプロイ頻度の向上

例えばライブラリのアップデートのような全体に影響が出る変更があった場合、自動テストのみで動作確認が完了したら工数を削減することができますし、ソフトウェアの品質も高めることができます。

またE2Eは特徴として「壊れやすい、時間がかかる」があります。 そのため方針としては、「不具合が発生するとビジネスへのネガティブインパクトが大きい場所を書く」ようにします。例えばコア機能の表示、作成、更新、削除などです。

実装のTips

実行環境で環境変数を切り替える

PlaywrightはローカルやGitHub ActionsのCI上など複数の環境にて実行が想定されます。 そのため環境変数を切り替えることができると便利です。

import { defineConfig, devices } from '@playwright/test'
import dotenv from 'dotenv'
import path from 'path'

if (process.env.TARGET) {
  dotenv.config({
    path: path.resolve(__dirname, '.', `.env.${process.env.TARGET}`),
  })
} else {
  dotenv.config()
}

export default defineConfig({
  ...

  use: {
    baseURL: process.env.BASE_URL,
  },
 
  ...
})

実行時に環境変数を渡してあげると環境変数を切り替えることができます。

TARGET=stg npx playwright test

playwright.dev

テスト用の開発サーバーを立てる

E2E実行時に特別な設定を実行したい場合や既存のローカル環境と区別したい場合は、E2E用のwebServerを立てることができます。

import { defineConfig } from '@playwright/test';

export default defineConfig({
  webServer: {
    command: 'npm run e2e',
    url: process.env.BASE_URL,
    reuseExistingServer: !process.env.CI,
    stdout: 'ignore',
    stderr: 'pipe',
    ignoreHTTPSErrors: true,
  },
});

ローカル環境をHTTPS化している場合はignoreHTTPSErrors: trueを設定することによってHTTPエラーを無視します。

またnpm run e2eを実行した結果、Viteにてe2eモードでサーバーを起動します。

  "scripts": {
    "e2e": "vite --port 3001 --mode e2e"
  },

playwright.dev

意識したポイント

  • 秒数指定はFlakyなテストに繋がるのでwaitForTimeout()は使わないようにする。
  • CI/CDに組み込む話は出るので実行時間は常に意識した方が良い。そのためテストケースごとに並列実行するfullyParallel: trueな前提でテストを書いた方が良い。テストケース単位で独立してテストできるようにする。

大変だったポイント

  • 何回実行してもテストケースが失敗しないようにする冪等性の担保。例えば1つのテストケースで作成から削除までを行う場合、作成後にテストが失敗したら不要なデータが残る。またリトライ処理で同じデータが複数個作成され、セレクタの指定時に Error: strict mode violation:で失敗する。アプリケーション側で対策する場合、名前にタイムスタンプを付与して常にユニークになるようにする。

これが出来ると良いと思ったポイント

  • ローカル環境のデータ整備。E2E実行前にデータの用意ができると楽。別コンテナを立てて手を動かす環境とは別で作業できると良い。

雑多なメモ

  • コードの自動生成機能があるが、案外動かないので、パターンを覚えて書いた方が自分は楽だった。page.locator('input[name="タイトル"]')のような指定は汎用的で使いやすい。
  • 開発初期はチートシートを参考にして要領を掴むのが良い。
  • テスト単位でタイムアウトの時間は設定できる。
  • アクセシビリティを意識してaria-labelを使ったコーディングをしているとpage.getByLabel()が使える。