CIでだけテストがたまに落ちる。ローカルで流すと通る。もう一度CIを回したら通る。Playwrightでテストを書いていれば、誰もが一度は踏むやつですよね。
1年目の頃の私なら、ここで「flakyだから」と`retries`を足して終わりにしていたと思います。でもこれ、実はけっこう危ない対応です。1回落ちただけで不安定判定してしまうと、本当は実装側のバグだったケースまで「retriesで丸めて見なかったことにする」ことになりかねません。私も過去に、リトライで通るからと放置していたテストが、実は非同期処理の実装バグを拾っていた、ということがありました。
まず「勘」で直さない
テストが落ちたとき、原因は大きく2つに分かれます。
- 実装側のバグ(本当に落ちるべくして落ちている)
- テスト側の不安定さ(待ち方やセレクタが甘くてタイミング依存になっている)
この見分けを勘でやると、だいたい後者だと決めつけがちです。人間、都合の悪い可能性は無視したくなりますよね。だからこそ、決めつける前に「本当に不安定なのか」を数字で確認する工程を挟むべきだと思っています。
`–repeat-each`で落ちる頻度を数値化する
Playwright Testには、同じテストをN回繰り返し実行する`–repeat-each`というCLIオプションがあります(執筆時点の公式ドキュメント確認済み)。疑わしいテストが見つかったら、まずはこう回してみましょう。
npx playwright test tests/checkout.spec.ts --repeat-each=20
これで同じテストを20回連続実行してくれます。20回中0回失敗なら、少なくともこの環境ではかなり安定していると言えますし、20回中5回落ちるなら、疑いようもなく不安定です。
ここで正直に書いておきたいのですが、「何回中何回落ちたら不安定と判断するか」の閾値に、業界標準のような決まった数字はありません。私が調べた範囲でも、ある記事では50回実行して3〜4回落ちるようなら不安定と判断していましたし、別の記事では10回中3回を目安にしていました。どちらも「その現場での経験則」であって、Playwright公式が定めた基準ではないんですね。なので「この記事ではこう言っている」という形で紹介はしますが、閾値は自分のチームのCI環境・実行時間の予算に合わせて決めるものだと思ってください。
並列実行時のノイズを減らしたいなら、`–workers=1`と組み合わせて1本ずつ直列で回すのも手です。CIの実行環境そのものが不安定要因になっているケースを切り分けたいときにも使えます。
npx playwright test tests/checkout.spec.ts --repeat-each=20 --workers=1
HTMLレポートの「Flaky」タグをまず見る
`–repeat-each`は自分で疑わしいテストに狙いを定めて回すコマンドです。でも「そもそもどのテストを疑えばいいのか」が分かっていないと使えません。ここで先に見るべきなのが、PlaywrightのHTMLレポートです。
`retries`を1以上に設定した状態でテストを実行すると、1回目は失敗したけれどリトライで成功したテストが「Flaky」として自動的に分類されます。
// playwright.config.ts
export default defineConfig({
retries: 2,
reporter: 'html',
});
npx playwright test
npx playwright show-report
レポートを開くと、Passed・Failed とは別に Flaky の件数が表示されます。ここに並んでいるテストが、まさに「リトライで救われている=本当は不安定な」候補です。CIを毎回リトライ任せで緑にしていると、この一覧を見落としたまま「なんとなく通っている」状態が続きます。まずはここを定期的にチェックする習慣が、`–repeat-each`より手前にある一番地味で効く工程だと思っています。
不安定さの発生源を潰す順番
Flaky候補が絞れたら、次は原因潰しです。順番はこうです。
1. 静的な`waitForTimeout`をやめる
「とりあえず1秒待つ」で通してきたコードがあれば、まずここを疑います。
// 悪い例
await page.waitForTimeout(1000);
await expect(page.locator('.result')).toHaveText('完了');
これ、環境が速ければ無駄待ちですし、環境が遅ければ足りません。`expect`系のアサーションは既定で自動的にリトライしながら条件を待ってくれるので、こう書き換えてみてください。素直に置き換えるほうが速くて安定します。
// 良い例
await expect(page.locator('.result')).toHaveText('完了');
非同期のAPIレスポンスなど、同期的な`expect`では書きにくい条件を待ちたい場合は`expect.poll`が使えます。任意の関数の戻り値を、条件を満たすまでポーリングしてくれるやーつです。
await expect.poll(async () => {
const res = await page.request.get('/api/jobs/123');
return res.status();
}, { timeout: 10000 }).toBe(200);
2. CSSセレクタをrole系ロケータに変える
クラス名が整理されただけでテストが軒並み落ちる。CSSセレクタでテストを書いていれば誰もが一度は経験することだと思います。
// 壊れやすい
await page.locator('.btn-submit-primary').click();
// 壊れにくい
await page.getByRole('button', { name: '送信' }).click();
`getByRole`はクラス名ではなく、ブラウザが認識している「役割と名前」を見てくれています。だからマークアップやスタイルをいじっても壊れにくいんですね。デザイナーさんがクラス名をリファクタしただけで大量に落ちるテストに悩まされている場合、ここを直すだけでFlaky件数がかなり減ります。
CIに`–repeat-each`をどう組み込むか
毎回のCIで全テストを`–repeat-each=20`で回すと、実行時間が単純に20倍になってしまいます。さすがにこれは現実的ではありません。私がやっているのは、疑わしいテストが出たときだけ手元かCIの手動トリガーで狙い撃ちする方法です。
name: flaky-check
on:
workflow_dispatch:
inputs:
spec:
description: '疑わしいテストファイルのパス'
required: true
jobs:
repeat:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npm ci
- run: npx playwright install --with-deps
- run: npx playwright test ${{ github.event.inputs.spec }} --repeat-each=20 --workers=1
普段のCIパイプラインは通常どおり流しつつ、Flakyタグが出たテストだけをこの専用ワークフローに投げる形です。全部を毎回20倍で回す必要はなく、疑わしいものだけをピンポイントで叩けば十分だと思っています。CI構築そのものが初めての方は、以前書いたGitHub ActionsでPlaywrightを自動実行する記事も参考にしてください。
ちなみにCI環境そのもののリソース不足が原因で不安定になっているケースもあります。並列数(`workers`)を絞ってCPU・メモリの奪い合いを減らすだけでFlakyが減った、という報告を見かけたこともあります。`–repeat-each`で数値化する前に、まずCIのworkers設定を見直すのも一つの手だと思います。
この順番を守る理由
①頻度を測る→②Flakyタグで候補を絞る→③発生源を潰す、という順番には理由があります。いきなり③から手を付けると、直したつもりで実は違う箇所が原因だった、という取り違えが起きるからです。私も過去にセレクタを疑って直したのに全然安定せず、あとから調べたら実装側の非同期処理が原因だった、ということがありました(このときは正直、時間を無駄にしたなと反省しました)。
逆にいうと、①②で「本当に不安定なテストはこれだ」と特定できていれば、③の修正がちゃんと効いたかどうかも同じ`–repeat-each`で再検証できます。直した後にもう一度20回回して、0回失敗になっていれば直った証拠です。ここまでやって初めて「直った」と言えると思っています。
正直に書いておくこと
今回紹介した閾値の数字(50回中3〜4回・10回中3回)は、いずれも私が調べた個別の記事の経験則であって、Playwright公式が推奨する基準ではありません。自分のCI環境で実際に何度か`–repeat-each`を回してみて、自分のチームなりの目安を作るのが結局は一番確実だと思います。また、`retries`を上げること自体は悪ではなく、あくまで「原因を特定せずに闇雲にretriesを足して蓋をする」ことが問題だという点は誤解しないでいただきたいところです。
まとめ
「たまに落ちるテスト」に出会ったら、まずは`–repeat-each`で頻度を数値化するか、HTMLレポートのFlakyタグを確認する。原因を測ってから、`waitForTimeout`とCSSセレクタという2大不安定要因を順番に潰す。この流れさえ守れば、勘で直して再発する、という遠回りはかなり減らせるはずです。
`–repeat-each`以外のCLIオプションももう少し詳しく知りたい方はPlaywright CLIオプション完全リファレンスもあわせてどうぞ。`–workers`や`–grep`の使い分けも書いています。皆さんの現場での閾値の決め方があれば、コメントで教えてもらえるとうれしいです。

コメント