PlaywrightのFlakyと向き合う
なぜPlaywrightテストがたまに失敗するのか
条件はかわらないのにたまに失敗するテストがある。。。
E2Eテストを書いている方なら、一度は経験したことがあるのではないでしょうか。コードは何も変えていないのに、テストが時々失敗する。これがいわゆる「フレイキーテスト」です。
フレイキーテストは開発チームにとって本当に厄介な存在ですね。CI/CDパイプラインが予期せず失敗して、リリースが遅れる。デバッグのために呼び出されて、結局「もう一回実行したら通りました」で終わる。テストへの信頼性が低下し、だんだんと「どうせフレイキーだから...」と無視されるようになってしまいます。
Playwrightでフレイキーテストが発生する主な原因は、タイミング問題、非同期処理の競合状態、外部サービスへの依存、そしてアニメーションやトランジションの影響などです。今回は、これらのフレイキーテストを見つけて、対処する方法を紹介します。
結論
-
--repeat-eachオプションでフレイキーテストを効率的に検出 - HTMLレポートでフレイキーテストを可視化して成功率を把握
- トレースビューアーで失敗の原因を詳細に分析
- 実践的な対処法でフレイキーテストを根本から解決
それでは、実際に試してみましょう!
検証環境
| 項目 | バージョン |
|---|---|
| @playwright/test | 1.53.2 |
| Node.js | 22.x |
| VSCode拡張 | Playwright Test for VSCode |
--repeat-eachオプションでフレイキーテストをあぶり出す
まずは、フレイキーテストを見つけることから始めましょう。Playwrightには--repeat-eachというオプションがあります。これを使うと、各テストを指定した回数だけ繰り返し実行します。
基本的な使い方
# 各テストを10回ずつ実行
npx playwright test --repeat-each=10
# 特定のテストファイルに対して20回実行
npx playwright test tests/login.spec.ts --repeat-each=20
# 並列実行と組み合わせて効率化(おすすめ!)
npx playwright test --repeat-each=5 --workers=4
# 特定のプロジェクトに対して実行
npx playwright test --repeat-each=10 --project=chromium
# タグ付きテストのみ繰り返し
npx playwright test --repeat-each=3 --grep="@critical"
開発環境で50回ぐらいやって3~4回ぐらい失敗すればそれはだいたいフレーキーです。しかもだいたい同じ箇所で失敗します。
CI環境で走らせる前にフレーキーなテストは潰し終わっているのが理想です。並列実行(--workers)と組み合わせると、マシンリソースがより枯渇するせいかよりあぶりだしやすくなります。
HTMLレポートでフレイキーテストを可視化する
フレイキーテストを見つけたら、次はPlaywrightのHTMLレポートで可視化して分析しましょう。
HTMLレポートの設定
import { defineConfig } from '@playwright/test';
export default defineConfig({
reporter: [
["html", { open: "never" }]
],
// リトライ設定(フレイキー検出に必須)
retries: process.env.CI ? 2 : 1,
});
HTMLレポートを開くと、フレイキーテストには特別な「Flaky」タグが付いて表示されます。成功率、失敗パターン、実行時間の分布なども確認できるので、問題の傾向が見えてきます。
# レポートを生成して開く
npx playwright show-report
レポート画面では、フレイキーテストだけをフィルタリングして表示することもできます。Timeline viewを使えば、どのタイミングで失敗しているかのパターンも分析できます。
VSCode拡張のトレースビューアーで失敗原因を突き止める
VSCode拡張(Playwright Test for VSCode)を使うと、トレースを直接VSCode内で表示できます。
VSCodeでのトレース
-
Test Explorerでテストを実行
- VSCodeのサイドバーでShow trace viewerにチェックをつける
- テストを実行
-
トレースビューアーで分析
- ステップごとのスクリーンショット
- DOM変化のタイムライン
- ネットワークリクエストの詳細
- コンソールログの確認
よくあるフレイキーパターンの特定
トレースビューアーで以下のパターンを確認すると、原因が特定しやすくなります
- 要素が見つからない: DOMの更新タイミングの問題
- ネットワークエラー: APIレスポンスの遅延
- タイムアウト: アニメーションや非同期処理の完了待ち
失敗の瞬間の前後を詳細に確認でき、フレイキーの原因特定に非常に有効です。
フレイキーテストを減らすための実践的アプローチ
タイミング問題の解決
// ❌ 悪い例 - 固定時間待機(あまり良くない。マシンリソースによって失敗しやすい)
await page.waitForTimeout(1000);
await page.click('#submit');
// ✅ 失敗しにくい例 - 条件待機
await page.waitForURL('/home');
await expect(page.locator('#submit')).toBeVisible();
await page.click('#submit');
ネットワーク依存の排除
外部APIに依存するテストは、ネットワークの状況でフレイキーになりがちです。モックを活用してみるか、下記で述べているポーリングを使ってみましょう(せっかくe2eしているのでモックよりはポーリングがいいかもしれない)
// APIモックの設定
await page.route('**/api/data', async route => {
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({ data: 'mocked' })
});
});
// リクエストの完了を待つ
const responsePromise = page.waitForResponse(
response => response.url().includes('/api/data') && response.status() === 200
);
await page.click('#fetch-data');
await responsePromise;
アニメーション・トランジションの処理
アニメーションもフレイキーテストの原因になります。
ポーリング
既存のアプリケーションでアニメーションを無効化するのは難しい場合もあります。その場合は、expect.pollを使ってアニメーションの影響を受けにくい状態を待つことができます。
実際にプロジェクトで使っている例を以下に示します。
/**
* ポーリング処理のためのユーティリティクラス
*/
class ActionWhile<T> {
constructor(
private fn: () => Promise<T>,
private options: {
intervals?: number[];
timeout?: number; // タイムアウト設定
} = {
intervals: [100],
timeout: 30000
},
) {}
/**
* 期待値と一致するまで待機する
* @param expectedValue - 期待する値
*/
async toBe(expectedValue: T): Promise<void> {
await expect.poll(this.fn, this.options).toBe(expectedValue);
}
}
/**
* ポーリング処理を開始する
* @param fn - ポーリングする関数
* @param options - ポーリングのオプション
* @returns ActionWhileインスタンス
*/
export function actionWhile<T>(
fn: () => Promise<T>,
options: {
intervals?: number[];
timeout?: number;
} = {
intervals: [500],
timeout: 30000
},
): ActionWhile<T> {
return new ActionWhile(fn, options);
}
/**
* 要素が表示されるまで待機する
* @param locator - 確認対象の要素のLocator
* @param options - ポーリングのオプション
* @returns 表示された要素のLocator
*/
export async function waitForVisible(
locator: Locator,
options: {
intervals?: number[];
timeout?: number;
} = {
intervals: [100],
timeout: 30000
},
): Promise<Locator> {
await actionWhile(async () => await locator.isVisible(), options).toBe(true);
return locator;
}
自作したactionWhileを使うと、だいたいのフレーキーな箇所は解決できます。
例えば、shadcnのSelectコンポーネントを使っていると、選択肢をクリックした後にアニメーションが発生して、次の操作が失敗することがあります。そんなときは、以下のようにwaitForVisibleを使って待機します。
~~省略~~
const selectForm = await stepProgress.step(`${label}が表示されるまで待機`, async () => {
const selectForm = this.page.getByLabel(label);
await waitForVisible(selectForm);
return selectForm;
});
await stepProgress.step(`${label}をクリックする`, async () => {
await selectForm.click();
const divisionList = this.page.getByRole("listbox");
await waitForVisible(divisionList);
});
~~省略~~
まとめ
-
検出:
--repeat-each=50で積極的にフレイキーテストを検出 - 分析: HTMLレポートで成功率と傾向を確認
- 原因特定: VSCodeトレースビューアーで詳細分析
-
対処:
actionWhileなどのユーティリティで根本解決
マシンと人間の思い込みのギャップがフレイキーの原因です。
他に対策があれば、ぜひ教えてください!
現場からは以上です ![]()
参考リンク
-
Playwright Documentation - Playwright公式ドキュメント
-
Debugging Tests - デバッグ方法のガイド
-
Trace Viewer - トレースビューアーの使い方
-
HTML Reporter - HTMLレポーターの設定
-
Playwright Test for VSCode - VSCode拡張機能
-
Best Practices - ベストプラクティスガイド
-
Release Notes - 最新機能の情報
