机制详解:从 Worker 进程隔离到源码级重试调度)
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 --retries3该选项在 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.tsTestSuite持有_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 会被整体纳入候选。retryStrategyimmediate 与 isolatedcommon/config.ts 定义了retryStrategy配置项取值immediate | isolated默认值为immediateconfigLoader.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 组内跨测试复用单个 PagePlaywright 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(); });JavaScriptCommonJS版本等价写法// 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 --retries2或配置文件兜底 flaky把 flaky 当作信号而非噪音报告器会把 flaky 单独计数并在汇总中展示长期 flaky 的测试应回到隔离测试 稳定等待的路线而不是无限加重试次数重试时清理副作用利用testInfo.retry在重试前清理缓存、数据库或服务器状态避免上一轮失败的残留影响本轮判定依赖测试用 serial 整组重试describe.serial保证失败组内后续测试被跳过且整组重跑但官方建议优先改造为相互隔离的独立测试配合 trace 模式定位 flakyon-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),仅供参考