💨

Playwrightを使ったWEBアプリのE2Eテスト作成

2025/01/17に公開

WEBアプリ E2Eテスト

初めに

現在自分が関わっているWEBアプリ作成プロジェクトでE2Eテストを作成練習した際に学んだことを記事として残しておきます。作成中プロジェクトならではの箇所もあるため、参考にできそうな箇所だけ参考にしてください。

概要

目的

エンドツーエンド(E2E)テストとは、ユーザーが実際にアプリを利用する手順をシミュレートすることで、アプリケーション全体の動作を検証するテストです。
コード化して自動化することでプログラミング言語のバージョン変更等の際にも即座に全体が適切に機能しているかを確認することが目的です。
導入に手間が掛かりますが、何かある度に手作業でテストをする無駄が省けます。

前提条件

自身の進行中作業の環境であるため、皆様の環境で動作するかは各自でご確認お願いします。

  • Node.js: v18 以上
  • TypeScript: v4.9 以上
  • テストランナー: Playwright (v1.49 以上)
  • 対応ブラウザ: Chrome, Firefox, Safari
  • OS: Windows, macOS, Linux (すべて対応)

セットアップ手順

環境変数の設定

テストコードにそのままアカウント情報を記載することを避けるため、プロジェクトルート下に.envファイルを作成します。

# .env.sample
USERNAME="test_runner"
PASSWORD="test_runner_pass"
ACCOUNTNAME="Runner Test"

プログラムの起動

プロジェクトルート下で下記コマンドを実行

  • ビルド
npm run build -w "" # -w以下でディレクトリを指定
  • 起動
npm start -w "" # -w以下でディレクトリを指定
# 再起動する場合はbash上でCtrl+Cを押して終了させてから再度このコマンドを実行

テストの実行方法

実行コマンド

プロジェクトルート下で下記を実行。必要に応じてオプションを追加します。

npx playwright test
    --ui # UIモードで実行 (クリック操作でテストを逐次実行可能)
    --headed # テスト実行を視覚的に確認可能
    --project "ブラウザ名" # webkit, firefoxなど実行するブラウザを指定 
    --debug # デバッグモードで実行

テスト構成サンプル

Project_Root
├──playwright.config.ts
├──e2e/
    ├── db-setup.ts     # テストが実行される前にデータベースをリセットする関数
    └── scenario1.spec.ts # 一連の処理成功パターンのテスト

新しいテストの作成方法

基本的なコード

基本の流れとしてはまず、Playwright Locatorsを使用してボタンやラベル、入力欄などを取得し、シミュレートしたい動作を追加します。ロケーターは様々あるため、公式サイトを確認しながら使用してください。

// e.g. ラベルから入力欄を取得して記入する
await page.getByLabel('User Name') // "User Name"と書かれたラベルを持った要素を取得
    .fill('John'); // 取得した要素に対して"John"と記入する

変数として格納して使用することも可能です。

const locator = page.getByRole('button', { name: '作成' });
await locator.click();

UIを使用したコード生成

下記を実行すると実際にUIを操作しながら自動的にE2Eテストを作成することができます。

一通りの動作を自動生成した後、テスト動作を確認しながら修正していきます。

npx playwright codegen http://localhost:3000/ # ローカル環境で実行する場合(本番環境ではこのURLを変更)

ログイン画面に対してcodegenを使用すると例として下記のようなコードが生成されます。

import { test, expect } from '@playwright/test';

test('test', async ({ page }) => {
  await page.goto('http://localhost:3000/');
  await page.getByRole('button', { name: 'ログイン' }).click();
  await page.getByRole('textbox', { name: 'Username' }).click();
  await page.getByRole('textbox', { name: 'Username' }).fill('test_runner');
  await page.getByRole('textbox', { name: 'Username' }).press('Tab');
  await page.getByRole('textbox', { name: 'Password' }).click();
  await page.getByRole('textbox', { name: 'Password' }).fill('test_runner_pass');
  await page.getByRole('button', { name: 'submit' }).click();
});

遭遇したエラーと対処法

コンポーネント表示前にクリック処理が実行され、エラーになるミス

例えばクリックシミュレートしたいボタンが表示される前にクリック処理が実行され、進行不能となる場合があります。

対処法:対象のコンポーネントが可視化されるまで待つようにします。waitFor()でも足りない場合は秒数指定で待つようにします。

await page.getByRole('button', { name: '新規登録' }).waitFor({ state: 'visible' });
// await page.waitForTimeout(2000); // どうしても処理に失敗する場合
await page.getByRole('button', { name: '新規登録' }).click();

各ブラウザで並列実行した場合にどれか一つがタイムアウトするエラー

webkit、chromium、firefoxで並列実行した場合、どれか一つの進行が間に合わずにタイムアウトする場合があります。

対処法:Playwright.config.tsのtimeout秒数を増やします。上手く行かない場合はブラウザ毎の仕様の違いによる問題が起きている場合があります。

export default defineConfig({
  globalSetup: './e2e/db-setup.ts', // テスト開始前に実行するセットアップ
  timeout: 60000, // 各テスト全体の最大実行時間
  ...
});

CSSセレクタの指定間違い

動的に割り当てられるボタンなどは実行ごとに指定先が変わる場合があります。

対処法:ラベル名やdivなど静的な要素で指定します。

// 悪い例:この指定方法では上手くクリック操作をシミュレートできない
await page.locator('.css-19bb58m').first().click();
await page.locator('#react-select-2-input').click();
// 良い例:静的な要素から指定するようにすると良い
await page.locator('div')
    .filter({ hasText: /^カテゴリーSelect\.\.\.$/ })
    .locator('svg')
    .click();

テスト用IDのない同名のボタン指定ミス

例えば「評価入力」というボタンが複数存在する場合、正常に指定できなくなります。

対処法1:各要素にIDを設定する。(そもそもこちらが推奨)
対処法2:対象のボタンがあるdivの上位divをセレクタ指定して,そのdiv内で「評価入力」ボタンを指定する。

// 悪い例:指定したボタンが複数存在し、希望の評価入力ボタンが押せない
await page.getByRole('link', { name: '評価入力' }).first().click();

// 良い例:同一div内のラベルなどから上位divを特定し、そこから辿っていく
const parentDiv = page.getByText("playwright test").locator('..').locator('..');
await parentDiv.waitFor({ state: 'visible' });
await parentDiv.getByText('評価入力').click();

希望通りのクリックシミュレートが出来ないミス

かなり特殊な例ですが、私のプロジェクトではレーダーチャートで評価値の入力シミュレートをする箇所があり、そこで各節点そのものの座標位置で指定してクリックシミュレートすると上手くできませんでした。

原因は通常のclick()が長方形範囲全体でクリックするものであり、上記のようなラジオボタンの入力範囲が隣接している場合、同時に二つ以上の入力範囲をクリックしようとしてしまうためです。

対処法:予め設定してある節点毎のIDを指定してクリックイベントを生成して実行

// 悪い例:codegenで自動生成されたもの。これでは画面が繰り返しスクロールするだけで押すことができない
await page.locator('path:nth-child(66)').click();

// 良い例:押したい節点のIDからクリック有効範囲を取得し、その中心をクリックするイベントを生成して実行
await page.evaluate((selector) => {
const element = document.querySelector(selector);
if (element) {
    const path = element as SVGPathElement;
    const length = path.getTotalLength();
    const midpoint = path.getPointAtLength(length / 2); // 弧の中心点を取得
    const clickEvent = new MouseEvent('click', {
    bubbles: true,
    cancelable: true,
    clientX: midpoint.x,
    clientY: midpoint.y,
    });
    path.dispatchEvent(clickEvent);
}
}, `[data-testId="${nodeId}"]`);

参考資料

Discussion