Playwright 测试重试(Retries)机制详解:从 Worker 进程隔离到源码级重试调度
【免费下载链接】playwrightPlaywright is a framework for Web Testing and Automation. It allows testing Chromium, Firefox and WebKit with a single API.项目地址: https://gitcode.com/GitHub_Trending/pl/playwright
本文围绕 Playwright 官方文档中的 Retries 指南展开:先讲清测试失败时 Worker 进程如何被整体丢弃与重建,再覆盖--retries命令行、playwright.config.ts配置、test.describe.configure({ retries })分组重试、TestInfo.retry运行时检测以及 Serial 模式整组重试等全部官方用法,并结合 runner/dispatcher.ts、common/config.ts 等仓库源码,说明重试候选如何被收集、隔离调度与分类为 flaky 的完整链路。
为什么需要重试:测试失败与 Worker 进程生命周期
测试重试(Retries)是一种在测试失败后自动重新运行的机制,专门用于应对**间歇性失败(flaky)**的测试。理解它的前提,是理解 Playwright Test 的 Worker 进程模型:
- 测试运行在worker 进程中。这些进程是独立的操作系统进程,由测试运行器(test runner)统一编排;
- 所有 worker 拥有完全一致的环境,每个 worker 都会启动自己独立的浏览器实例。
文档用一段典型用例演示了三种场景。假设存在如下测试文件:
import { test } from '@playwright/test'; test.describe('suite', () => { test.beforeAll(async () => { /* ... */ }); test('first good', async ({ page }) => { /* ... */ }); test('second flaky', async ({ page }) => { /* ... */ }); test('third good', async ({ page }) => { /* ... */ }); test.afterAll(async () => { /* ... */ }); });场景一:全部通过。所有测试按顺序在同一个 worker 进程中运行:
* Worker process starts * beforeAll hook runs * first good passes * second flaky passes * third good passes * afterAll hook runs场景二:任一测试失败。Playwright Test 会连同浏览器一起丢弃整个 worker 进程,并启动一个新进程。测试从下一个测试开始在新 worker 中继续:
* Worker process #1 starts * beforeAll hook runs * first good passes * second flaky fails * afterAll hook runs * Worker process #2 starts * beforeAll hook runs again * third good passes * afterAll hook runs场景三:启用重试后。第二个 worker 进程会以"重试失败的测试"作为起点,然后继续后续测试:
* Worker process #1 starts * beforeAll hook runs * first good passes * second flaky fails * afterAll hook runs * Worker process #2 starts * beforeAll hook runs again * second flaky is retried and passes * third good passes * afterAll hook runs这种"失败即换进程"的方案对相互独立的测试非常有效,并保证了失败的测试不会污染健康的测试。
这一设计在源码中可以得到印证。在 runner/dispatcher.ts 中,Job的onDone路径在 job 结束后会调度剩余测试与重试任务;当 worker 进程意外退出时(onExit),会生成worker process exited unexpectedly错误并跳过相关测试。而每个 job 完成后,runInWorker通过 IPC 把retry: test.results.length传给 worker——当前已运行次数即本次的重试序号,这保证了新 worker 从正确的重试轮次开始执行。
配置重试:命令行与配置文件
Playwright 支持测试重试:启用后,失败的测试会反复重试,直到通过或达到最大重试次数。默认情况下失败的测试不会被重试。
命令行方式
# 给失败的测试 3 次重试机会 npx playwright test --retries=3该选项在 program.ts 中定义,官方描述为:
['--retries <retries>', { description: `Maximum retry count for flaky tests, zero for no retries (default: no retries)` }]即:传入0表示不重试,不传时默认为不重试。
配置文件方式
import { defineConfig } from '@playwright/test'; export default defineConfig({ // Give failing tests 3 retry attempts retries: 3, });从源码结构看,retries的生效顺序在 common/config.ts 中明确定义:
retries: takeFirst(configCLIOverrides.retries, projectConfig.retries, config.retries, 0),即优先级为:CLI--retries> 单个 project 的retries> 全局config.retries> 默认 0。这意味着你可以在projects数组里为不同浏览器项目配置不同的重试次数,同时仍可用命令行临时覆盖。
结果分类:passed、flaky、failed
启用重试后,Playwright Test 会把测试归为三类:
- passed:首次运行即通过的测试;
- flaky:首次运行失败、但在重试后通过的测试;
- failed:首次运行失败且所有重试均失败的测试。
一次典型的运行输出如下:
Running 3 tests using 1 worker ✓ example.spec.ts:4:2 › first passes (438ms) x example.spec.ts:5:2 › second flaky (691ms) ✓ example.spec.ts:5:2 › second flaky (522ms) ✓ example.spec.ts:6:2 › third passes (932ms) 1 flaky example.spec.ts:5:2 › second flaky 2 passed (4s)这一分类逻辑同样体现在仓库的报告器实现中。在 reporters/base.ts 中,TestSummary包含独立的flaky: TestCase[]字段,摘要消息会把 flaky 测试以黄色高亮单独列出(${flaky.length} flaky),并在失败明细中优先展示 flaky 项。因此flaky 不会导致整个 run 失败,但会在汇总中显式暴露,方便持续追踪不稳定测试。
运行时检测重试:TestInfo.retry 与分组配置
TestInfo.retry
可以在测试内部通过testInfo.retry属性检测当前是否为重试(0 表示首次运行),该属性对任何测试、hook 和 fixture 都可用。典型用途是在重试前清理服务端状态:
import { test, expect } from '@playwright/test'; test('my test', async ({ page }, testInfo) => { if (testInfo.retry) await cleanSomeCachesOnTheServer(); // ... });test.describe.configure 指定分组重试
针对特定测试组或单个文件,可以用test.describe.configure({ retries })覆盖全局配置:
import { test, expect } from '@playwright/test'; test.describe(() => { // All tests in this describe group will get 2 retry attempts. test.describe.configure({ retries: 2 }); test('test 1', async ({ page }) => { // ... }); test('test 2', async ({ page }) => { // ... }); });该机制在源码中的落点是 common/test.ts:TestSuite持有_retries字段,TestCase持有retries字段,且TestCase.retries初始值为0;当配置序列化时,suite 层的_retries会被展开到该组下的每个测试上,最终由 dispatcher 用test.results.length < test.retries + 1判断该测试还有没有重试额度。
Serial 模式:整组测试一起重试
使用test.describe.serial(等价于test.describe.configure({ mode: 'serial' }))可以分组管理相互依赖的测试,确保它们始终在一起按顺序运行。组内任一测试失败时,其后的所有测试会被跳过;而整组测试会作为一个整体一起重试。
import { test } from '@playwright/test'; test.describe.configure({ mode: 'serial' }); test.beforeAll(async () => { /* ... */ }); test('first good', async ({ page }) => { /* ... */ }); test('second flaky', async ({ page }) => { /* ... */ }); test('third good', async ({ page }) => { /* ... */ });未启用重试时,失败后的测试全部被跳过:
* Worker process #1: * beforeAll hook runs * first good passes * second flaky fails * third good is skipped entirely启用重试时,整组一起重试:
* Worker process #1: * beforeAll hook runs * first good passes * second flaky fails * third good is skipped * Worker process #2: * beforeAll hook runs again * first good passes again * second flaky passes * third good passes源码印证了"整组重试"的调度细节。在 runner/dispatcher.ts 中,onDone会为每个失败测试向上查找最外层 serial suite(_parallelMode === 'serial'),把该 suite 下全部测试加入重试候选集:
for (const serialSuite of serialSuitesWithFailures) { // Add all tests from failed serial suites for possible retry. // These will only be retried together, because they have the same // "retries" setting and the same number of previous runs. serialSuite.allTests().forEach(test => retryCandidates.add(test)); }同时,dispatcher 还会把"属于某个已失败 serial suite 的后续剩余测试"整体标记为跳过(_massSkipTestsFromRemaining),这正是文档中 "third good is skipped" 的实现来源。
注意(官方建议):通常更好的做法是让测试保持相互隔离,从而能够被高效地独立运行和重试。Serial 模式应作为依赖场景的例外手段。
源码视角:重试如何被调度(isolated 与 immediate)
除了文档显式描述的行为,仓库源码还揭示了重试调度的两个关键策略,适合希望深入理解 runner 内部机制的读者。
重试候选的收集条件
在 runner/dispatcher.ts 中,job 结束后的重试候选集由三个条件共同决定:
- 非可重试错误的测试被排除:
_failedWithNonRetriableError命中的失败测试(例如 plan 标注跳过等)不会进入重试; - 重试额度检查:只有满足
test.results.length < test.retries + 1的测试才会被安排重试,其中test.results.length即已运行次数; - serial 组扩展:失败测试所在的最外层 serial suite 会被整体纳入候选。
retryStrategy:immediate 与 isolated
common/config.ts 定义了retryStrategy配置项,取值'immediate' | 'isolated',默认值为'immediate'(configLoader.ts 对非法取值会直接报错)。两种策略在 dispatcher.ts 中的行为差异为:
for (const test of retryCandidates) { if (test.results.length < test.retries + 1) { // Immediate retries run together with the remaining tests, in a single job. if (this._testRun.config.retryStrategy === 'immediate') remaining.push(test); else isolatedRetries.push(test); } }- immediate:重试任务与剩余未跑的测试合并在同一个新 job 中执行,运行效率更高;
- isolated:重试任务被拆出独立的
isolatedRetriesJob,只包含失败测试,避免与正常测试混合——便于隔离观察不稳定测试。
此外,program.ts 中 trace 模式还有一组与重试联动的取值:on-first-retry、on-all-retries、retain-on-failure、retain-on-first-failure、retain-on-failure-and-retries。例如配置trace: { mode: 'on-first-retry' }可以在首次失败后的重试中自动采集 trace,帮助定位 flaky 根因。
在 Serial 组内跨测试复用单个 Page
Playwright Test 默认为每个测试创建隔离的Page对象。如果确实需要跨测试复用同一个Page,可以在test.beforeAll中自行创建、在test.afterAll中关闭(通常需要配合 serial 模式):
import { test, type Page } from '@playwright/test'; test.describe.configure({ mode: 'serial' }); let page: Page; test.beforeAll(async ({ browser }) => { page = await browser.newPage(); }); test.afterAll(async () => { await page.close(); }); test('runs first', async () => { await page.goto('https://playwright.dev/'); }); test('runs second', async () => { await page.getByText('Get Started').click(); });JavaScript(CommonJS)版本等价写法:
// @ts-check const { test } = require('@playwright/test'); test.describe.configure({ mode: 'serial' }); /** @type {import('@playwright/test').Page} */ let page; test.beforeAll(async ({ browser }) => { page = await browser.newPage(); }); test.afterAll(async () => { await page.close(); }); test('runs first', async () => { await page.goto('https://playwright.dev/'); }); test('runs second', async () => { await page.getByText('Get Started').click(); });这种模式常用于登录态复用等场景:第一个测试完成登录后,后续测试直接复用同一浏览器上下文,避免重复鉴权。
最佳实践小结
- 默认不重试,按需在 CI 中开启:本地开发时保持默认(无重试)以快速暴露问题,CI 中用
npx playwright test --retries=2或配置文件兜底 flaky; - 把 flaky 当作信号而非噪音:报告器会把 flaky 单独计数并在汇总中展示,长期 flaky 的测试应回到"隔离测试 + 稳定等待"的路线,而不是无限加重试次数;
- 重试时清理副作用:利用
testInfo.retry在重试前清理缓存、数据库或服务器状态,避免上一轮失败的残留影响本轮判定; - 依赖测试用 serial + 整组重试:
describe.serial保证失败组内后续测试被跳过且整组重跑,但官方建议优先改造为相互隔离的独立测试; - 配合 trace 模式定位 flaky:
on-first-retry/on-all-retries等 trace 模式与重试联动,只记录失败路径的追踪数据,控制产物体积。
相关参考:配置指南、测试分类指南、重试调度实现 dispatcher.ts、配置解析 config.ts、报告器 flaky 分类 base.ts。
【免费下载链接】playwrightPlaywright is a framework for Web Testing and Automation. It allows testing Chromium, Firefox and WebKit with a single API.项目地址: https://gitcode.com/GitHub_Trending/pl/playwright
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考