Playwright ベストプラクティス
この章では、Playwright テストの作成と保守における最も重要なベストプラクティスをまとめ、安定した保守可能なテストの作成を支援します。
テスト哲学
ユーザーに見える動作をテストする
自動化テストは、エンドユーザーが見たり操作したりできる内容を検証すべきであり、コードの実装詳細を検証すべきではありません。
例えば、テストはページに正しいテキストが表示されること、ボタンがクリックできること、フォーム送信後に正しいページへ遷移することを検証すべきであり、特定の JavaScript 関数の戻り値や DOM の内部構造を検証するべきではありません。
例
// 非推奨:実装詳細をテストする
expect(await page.evaluate(() => window.__store.getState().user.name))
.toBe('サンプル');
// 推奨:ユーザーが見る内容をテストする
await expect(page.getByText('欢迎,サンプル')).toBeVisible();
expect(await page.evaluate(() => window.__store.getState().user.name))
.toBe('サンプル');
// 推奨:ユーザーが見る内容をテストする
await expect(page.getByText('欢迎,サンプル')).toBeVisible();
テスト分離
各テストは完全に独立している他のテストから独立しており、前のテストの状態に依存しない必要があります。
Playwright はデフォルトで独立した Context を提供しますが、テストの実行順序に依存しないようにすることも確実に行うべきです。
例
// 非推奨:テスト間で相互に依存する
let createdId;
test('创建资源', async ({ request }) => {
const resp = await request.post('/api/items');
createdId = (await resp.json()).id; // 共有状態
});
test('作成されたリソースを使用', async ({ page }) => {
await page.goto(`/items/${createdId}`); // 前のテストの結果に依存する
});
// 推奨:各テストは自己完結している
test('创建并使用资源', async ({ request, page }) => {
const resp = await request.post('/api/items');
const id = (await resp.json()).id;
await page.goto(`/items/${id}`);
await expect(page.getByText('资源详情')).toBeVisible();
});
let createdId;
test('创建资源', async ({ request }) => {
const resp = await request.post('/api/items');
createdId = (await resp.json()).id; // 共有状態
});
test('作成されたリソースを使用', async ({ page }) => {
await page.goto(`/items/${createdId}`); // 前のテストの結果に依存する
});
// 推奨:各テストは自己完結している
test('创建并使用资源', async ({ request, page }) => {
const resp = await request.post('/api/items');
const id = (await resp.json()).id;
await page.goto(`/items/${id}`);
await expect(page.getByText('资源详情')).toBeVisible();
});
Locator 優先順位の原則
推奨される順序で Locator を使用すると、テストの安定性を向上できます。
| 優先順位 | メソッド | 適用シナリオ |
|---|---|---|
| 1 | getByRole() | 要素が明確な ARIA ロールを持つ |
| 2 | getByLabel() | フォーム要素が label に関連付けられている |
| 3 | getByPlaceholder() | 入力欄に placeholder がある |
| 4 | getByText() | 要素が明確なテキストを持つ |
| 5 | getByAltText() | 画像に alt 属性がある |
| 6 | getByTitle() | 要素に title 属性がある |
| 7 | getByTestId() | フォールバック |
| 8 | locator() | CSS/XPath、最後の選択肢 |
CSS クラス名をロケーターとして使用しないようにしてください。
クラス名はスタイルのリファクタリングで変更されやすく、テストが広範囲で失敗する原因になります。
不必要な待機を避ける
Playwright の自動待機機構を利用し、手動待機を避けるsleep 或 waitForTimeout。
例
// 非推奨:ハードコードされた待機
await page.waitForTimeout(3000);
await page.getByText('数据加载完成').click();
// 推奨:自動待機とアサーションに依存する
await expect(page.getByText('数据加载中...')).toBeHidden({ timeout: 10000 });
await page.getByText('数据加载完成').click();
await page.waitForTimeout(3000);
await page.getByText('数据加载完成').click();
// 推奨:自動待機とアサーションに依存する
await expect(page.getByText('数据加载中...')).toBeHidden({ timeout: 10000 });
await page.getByText('数据加载完成').click();
サードパーティサービスをテストしない
制御できる内容のみをテストし、サードパーティサービスの可用性に依存しないでください。
例
// 非推奨:外部 CDN や API に依存する
await page.goto('https://www.example.com/');
// ページは Google Analytics や外部フォントなどを読み込む可能性がある
// 推奨:サードパーティリクエストをインターセプトし、外部サービスをモックする
await page.route('**/*analytics*', route => route.abort());
await page.route('**/external-api/**', route => {
route.fulfill({ json: { status: 'ok' } });
});
await page.goto('https://www.example.com/');
// ページは Google Analytics や外部フォントなどを読み込む可能性がある
// 推奨:サードパーティリクエストをインターセプトし、外部サービスをモックする
await page.route('**/*analytics*', route => route.abort());
await page.route('**/external-api/**', route => {
route.fulfill({ json: { status: 'ok' } });
});
expect.soft() ソフトアサーションを使用する
複数の独立した条件をチェックする場合、ソフトアサーションは最初の失敗で停止するのではなく、すべての失敗を一度に報告できます。
例
// 推奨:ソフトアサーションで全フィールドを一度に検証する
await expect.soft(page.getByLabel('用户名')).toBeVisible();
await expect.soft(page.getByLabel('密码')).toBeVisible();
await expect.soft(page.getByLabel('邮箱')).toBeVisible();
await expect.soft(page.getByRole('button', { name: '注册' })).toBeEnabled();
// 複数のフィールドが欠落している場合、一度にすべて報告する
await expect.soft(page.getByLabel('用户名')).toBeVisible();
await expect.soft(page.getByLabel('密码')).toBeVisible();
await expect.soft(page.getByLabel('邮箱')).toBeVisible();
await expect.soft(page.getByRole('button', { name: '注册' })).toBeEnabled();
// 複数のフィールドが欠落している場合、一度にすべて報告する
テストファイルを適切に構成する
| プラクティス | 説明 |
|---|---|
| 機能モジュールごとにファイルを分ける | login.spec.ts、checkout.spec.ts 等 |
| describe を使用してグループ化する | 関連するテストを配置するtest.describe() 中 |
| beforeEach を有効に使う | 繰り返しのナビゲーションと準備手順を抽出するbeforeEach |
| テスト名を説明的にする | 良い名前は「何をしたか」と「何を期待するか」を説明できます |
| 共通コードを抽出する | 3 つ以上のファイルで使用されるヘルパーは共有モジュールに抽出する |
| DRY にしすぎない | 2〜3 行の重複は、過度な抽象化よりも保守が容易です |
よくある問題のトラブルシューティング
テストの不安定さ(flaky)
不安定なテストの原因と解決策:
| 原因 | 解決策 |
|---|---|
| ハードコードされた sleep が十分でない | 固定待機の代わりに自動待機機構を使用する |
| サードパーティサービスに依存する | 使用してroute()外部リクエストをモックする |
| アニメーションによる要素の位置変化 | 設定でanimations: 'disabled' |
| テスト間のデータ残存 | 各テストが独立してデータを作成・クリーンアップすることを確実にする |
| 時間に関連するロジック | 使用するpage.clock固定時間 |
Locator が要素を見つけられない
トラブルシューティング手順:
- 要素が DOM に存在するかを確認する(
toBeAttached()ではなくtoBeVisible()) - 要素がビューポート内にあるか確認する
- iframe にネストされているか確認する
- UI Mode で Pick Locator を使用してロケーターを確認する
- 複数の一致要素があるか確認する(Locator はデフォルトで厳格モードを要求します)