news 2026/9/7 7:08:13

Playwright 测试重试(Retries)机制详解:从 Worker 进程隔离到源码级重试调度

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Playwright 测试重试(Retries)机制详解:从 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 中,JobonDone路径在 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 结束后的重试候选集由三个条件共同决定:

  1. 非可重试错误的测试被排除_failedWithNonRetriableError命中的失败测试(例如 plan 标注跳过等)不会进入重试;
  2. 重试额度检查:只有满足test.results.length < test.retries + 1的测试才会被安排重试,其中test.results.length即已运行次数;
  3. 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-retryon-all-retriesretain-on-failureretain-on-first-failureretain-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(); });

这种模式常用于登录态复用等场景:第一个测试完成登录后,后续测试直接复用同一浏览器上下文,避免重复鉴权。

最佳实践小结

  1. 默认不重试,按需在 CI 中开启:本地开发时保持默认(无重试)以快速暴露问题,CI 中用npx playwright test --retries=2或配置文件兜底 flaky;
  2. 把 flaky 当作信号而非噪音:报告器会把 flaky 单独计数并在汇总中展示,长期 flaky 的测试应回到"隔离测试 + 稳定等待"的路线,而不是无限加重试次数;
  3. 重试时清理副作用:利用testInfo.retry在重试前清理缓存、数据库或服务器状态,避免上一轮失败的残留影响本轮判定;
  4. 依赖测试用 serial + 整组重试describe.serial保证失败组内后续测试被跳过且整组重跑,但官方建议优先改造为相互隔离的独立测试;
  5. 配合 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),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/7 7:07:36

游戏卡死怎么办?三角洲行动电脑死机排查与解决全指南

玩《三角洲行动》的时候电脑突然卡死&#xff0c;屏幕定格、鼠标失灵、声音消失&#xff0c;只能强制重启&#xff0c;重启之后又一切正常。这种问题反复出现几次之后&#xff0c;很多人第一反应是游戏优化不行&#xff0c;或者是电脑配置太差。但实际上&#xff0c;“卡死”这…

作者头像 李华
网站建设 2026/9/7 7:06:56

PSN生日找回工具源码解析:自动化账号恢复的工程实践

简介&#xff1a;面向PSN/SEN账号出生日期恢复场景的开源工具源码&#xff0c;程序基于Qt 5.1.0编写&#xff0c;采用GPLv3/LGPLv2.1双许可证发布&#xff0c;适合需要研究账号恢复逻辑、学习Qt跨平台GUI开发或进行二次开发的工程师。资源包共52个文件&#xff0c;压缩后约137K…

作者头像 李华
网站建设 2026/9/7 7:06:42

网络准入控制从认证到合规的落地指南

简介&#xff1a;大势至网络准入控制系统 v8.0 是一套面向企业及单位局域网管理场景的准入控制工具&#xff0c;适用于需要严格管控内部网络接入行为的网管人员。该软件可有效阻止外来电脑或移动设备私自接入公司网络&#xff0c;同时支持隔离内外网互访、禁用修改IP/MAC、防控…

作者头像 李华
网站建设 2026/9/7 7:05:29

3 步装好猫抓插件:免费视频资源嗅探与 M3U8 下载的完整指南

3 步装好猫抓插件&#xff1a;免费视频资源嗅探与 M3U8 下载的完整指南 【免费下载链接】cat-catch 猫抓 浏览器资源嗅探扩展 / cat-catch Browser Resource Sniffing Extension 项目地址: https://gitcode.com/GitHub_Trending/ca/cat-catch 网页上正在播放的视频&…

作者头像 李华
网站建设 2026/9/7 7:05:16

猫抓浏览器插件:一次装好网页视频下载与M3U8合并解析

猫抓浏览器插件&#xff1a;一次装好网页视频下载与M3U8合并解析 【免费下载链接】cat-catch 猫抓 浏览器资源嗅探扩展 / cat-catch Browser Resource Sniffing Extension 项目地址: https://gitcode.com/GitHub_Trending/ca/cat-catch 猫抓&#xff08;cat-catch&#…

作者头像 李华