site logo

Marico's space

分层 Playwright E2E 策略:从 PR 烟雾测试到生产验证

服务器技术 2026-06-23 14:50:17 6

最近在给团队搭 Playwright E2E 框架,从项目结构、标签策略到 CI/CD 分层跑法,折腾了小两个月。这篇把踩过的坑和最终的方案捋清楚,供有类似需求的同学参考。

分层 Playwright E2E 策略:从 PR 烟雾测试到生产验证

不做多余铺垫,直接进正题。假设你的产品够大:多个独立功能域(登录注册、下单支付、搜索、消息、计费、第三方集成等),测试要跑在从本地开发环境到阿里云多地域生产环境的全链路。

E2E 测试写到一定规模,核心矛盾就三个:跑得太慢(PR 合并等半天)、失败信号不靠谱(假阳性太多)、失败后定位不到人。下面的所有设计决策都是围绕这三个点来的。

框架配置:分层 + 按项目隔离

共享基础配置 + 薄薄的本地覆盖层

所有项目共享一份 playwright.base.config.ts,定义通用的报告器、失败时截图/录屏、默认超时等横切配置。每个子项目再按需覆盖。这样改一处全局配置,所有域都能生效,不会出现改了这忘了那的情况。

项目划分 = 按功能域隔离

不要搞一个大测试池子按文件名分。按功能域拆成独立的 Playwright 项目——checkoutsearchonboardingmessagingbilling 等,每个项目指向自己的 testDir。这样做有两个好处:

  1. CI 并行:每个域单独跑一个 Job,互不干扰。
  2. 失败路由:哪个域的 Job 红了,直接发给对应的负责团队,而不是扔到"测试挂了"的公共告警里。

重域拆分:按耗时切,不按名字切

总有一个域会变成拖后腿的那个——通常是带媒体上传、图像处理、AI 生成等耗时操作的业务。当单个域开始主导整体耗时,就把它拆成多个项目,共享同一套代码目录但按 spec 文件分组:

  • <domain>-core —— 快速 UI 逻辑的用例
  • <domain>-heavy —— 耗时的媒体/处理/生成用例
  • <domain>-endurance —— 隔离出来的超长耗时用例(后面单独说)

关键经验:按实际耗时均衡拆分,不要按功能名称想当然。 目标是让各个 Job 的实际耗时接近。定期看 HTML 报告中每个 spec 的耗时,有必要就重新分配——凭感觉分的组迟早有一个闲着、另一个卡脖子。

浏览器启动参数:需要才加

涉及设备采集的用例需要 Chromium 能接 mock 媒体流,不然本地跑会报硬件错误:

launchOptions: { args: [ '--use-fake-device-for-media-stream', '--auto-accept-camera-and-microphone-capture', '--autoplay-policy=no-user-gesture-required', ],
}

这些参数只加在需要的项目上,全局乱加只会让不需要的域也背着无用的启动开销。

一个小坑:别让 .env 覆盖命令行参数

加载 .env 时用 override: false

dotenv.config({ path: '../.env', override: false });

原因很重要——这是个静默 Bug:加了 override: true 的话,你命令行传的 APP_ENVIRONMENT=production pnpm exec playwright test … 会在 setup 运行前被 .env 里的默认值覆盖,结果就是"以为在跑生产环境"实际上跑的是预发。override: false 让命令行参数的优先级最高,.env 只填命令行没设置的坑。

标签策略:两个正交维度

这是大多数团队投入最少的部分,但恰恰是让整套测试可维护、可追溯的关键。用 Playwright 的运行时 tag 属性维护两个正交的标签维度。

 维度 1 — 可追溯性 维度 2 — 运行层级 (对标哪个测试用例?) (什么时候跑?) ┌──────────────────┐ ┌────────────────────────┐ 一个测试 ──────►│ @TC042 │ ──加───►│ @smoke │ │ (对接测试管理库 │ │ @production-validation │ │ 的稳定关联键) │ │ @endurance / (无标签) │ └──────────────────┘ └────────────────────────┘ 文件重命名不破坏关联 决定走哪条流水线

维度一:可追溯性——每个测试带稳定用例 ID

每个 test/describe 带上 @TCxxx 标签,对应测试管理系统里的某行数据。这个标签是对 spec 和测试用例记录的稳定关联键——文件改名、重构都不会打断这个链路。

test.describe('Complete checkout with saved card', { tag: ['@TC042'] }, () => { ... });

为什么用运行时的 tag 而不是标题里的字符串或 JSDoc 注释?

  1. 报告器输出:Playwright 的 JSON reporter 会输出 tags: [...],自动化对账任务可以直接同步结果到测试管理系统。JSDoc 根本到不了报告器;标题里的前缀还要额外解析。
  2. CLI 过滤--grep @TC042 跑单个用例;--grep @smoke 跑整个层级。
  3. 工具链兼容:TestRail / Xray / Zephyr / Qase 这些平台的 reporter 都消费运行时 tag,生态一致。

多用例标签——仅限顺序依赖的场景。 当几个测试用例其实是同一条用户旅程的连续步骤(比如第三方集成:连接 → 拉数据 → 执行操作 → 推送结果),共用认证/状态,只在文件里打多个标签,然后用 test.step('TCxxx: …') 给每步命名,这样报告里失败仍能定位到具体步骤:

test('connect, fetch, and push to the external system', async ({ page }) => { await test.step('TC101: connect the integration', async () => { ... }); await test.step('TC102: view connected details', async () => { ... }); await test.step('TC103: push a record', async () => { ... });
});

判断标准:这些场景能不能在任何顺序、独立用全新状态跑? 能 → 拆成独立测试。不能,后一步依赖前一步 → 合并成多用例标签的单个测试。 把有依赖的旅程拆开跑,意味着每个步骤都要单独承担认证、远程连接和 fixture 初始化的成本。

维度二:运行层级——决定走哪条流水线

跟用例 ID 无关,每个测试自己决定属于哪个运行层级:

标签 触发时机
@smoke 每个 PR(精心挑选的子集)
@production-validation 每次生产发版后,fan-out 到各地域
(无层级标签) 完整回归,夜间跑
@endurance 独立低频定时任务专用

烟雾测试层:快、精选、每个 PR 必跑

烟雾测试是常驻的 PR 卡点,设计上要有约束:

  • QA 主导选品,不是研发。 一个 spec 能进烟雾,条件是测试管理系统里这行的"smoke"勾选框打上了。要加/删 spec,先改那个勾,标签同步更新。这样 smoke 列表由一个团队负责,不会随时间膨胀失控。
  • 按域矩阵并行。 每个域的 smoke 单独跑一个 Job,每个 Job 只初始化该域需要的账号队列,worker 数固定为 1。
  • 设定耗时预算。 比如要求 PR smoke P95 不超过 5 分钟。因为各域并行,预算卡的是单个 Job,不是总耗时。这个预算是约束——谁想悄悄塞一个耗时好几分钟的 spec 进 smoke,就得面对这个硬指标。

超长耗时用例(比如一个耗时十几分钟、跑实时音视频的用例)就是反例:它不能进 smoke,更不能进夜间回归,要单独开一个 Playwright 项目,独立 Job,独立定时任务。

经验:把异常耗时的测试单独开辟隔离车道,永不让它的慢阻塞合并队列。

Worker 调优:看最弱的共享依赖,别看 CPU 核数

基础配置参考:

// CI = 3 workers,本地 = 4(顺序 IdP 登录的安全上限)
// 可通过 PLAYWRIGHT_WORKERS 环境变量覆盖
export const NUM_WORKERS = process.env.PLAYWRIGHT_WORKERS ? parseInt(process.env.PLAYWRIGHT_WORKERS, 10) : process.env.CI ? 3 : 4;

非直觉的经验:worker 数量上限取决于最弱的共享依赖,不是 CI 机器的 CPU 核数。

常见的瓶颈有两类:一是身份提供商(IdP)对短时间内并发登录的容忍度有限,二是被测环境的共享资源承载能力。worker 数调太高,不仅不会变快,反而会产生更多"失败"——这些失败本质是后端或 IdP 被打满了,但看起来像测试抖动。把 worker 数做成环境变量驱动的旋钮(PLAYWRIGHT_WORKERS),就能针对不同环境调参,不用改代码。

生产验证:多地域、发版触发、体积刻意保持小

每次生产发版后——发版节奏快的团队甚至会更频繁地跑——执行一套小而稳定的核心流程用例,在所有地理区域 fan-out 跑,通过手动触发的 workflow 调度。设计原则:

  • 地域 = 地域锁定的登录账号。 用户所属地域决定后端路由,所以"跑这个 spec 针对 X 地域"等于"用 X 地域的账号登录"。Workflow 把对应的地域 API base URL 传进去,确保 admin/setup 相关调用打对后端。
  • 静态账号,不在生产环境动态创建。 预发/测试环境动态创建临时账号,生产验证用的是加密存储的固定账号集合(按地域 key)。生产环境关闭动态创建,还有一层纵深防御:禁止调用内部管理接口。E2E 套件绝对不能意外在生产环境创建或修改数据。
  • 地域相关的 skip 必须显式加门控。 某个地域渲染不同 UI 或有已知后端问题,skip 要用地域环境变量(除了生产环境外全为 inert)做门控,注释里写清楚后续处理方案。skip 要可见、临时,不能是静默丢失的覆盖。
  • QA 负责维护清单。 生产验证用例集刻意保持小且稳定;研发要加新用例必须 QA 确认。用 playwright test --grep @production-validation --list 查看当前清单,作为唯一事实来源。

分层运行模型一览

PR 打开 ──────────────► @smoke (按域矩阵并行,1 worker,<5 分钟预算) │
夜间 ────────────────► 完整回归 (无层级标签的全部用例) │
每次发版 ────────────► @production-validation (多地域 fan-out,静态账号) (独立车道) ──────────► @endurance (独立定时任务,隔离项目)

每个层级在覆盖范围和速度之间做有意识的取舍。PR 获得快速、窄范围的反馈;夜间回归获得广度;生产获得小而高置信度的核心路径检查,覆盖多地域。

如果重头来,我会怎么跟另一个团队说

  1. 测试规模上来之前先把标签体系定清楚。 两个维度——追溯用例 ID + 运行层级标签——等你有 50 个以上测试的时候就知道值回票价了。
  2. Worker 数盯着最弱的共享依赖调,做成环境变量。 CI 机器的核数很少是真正的上限。
  3. 给异常测试单独开道。 一个耗时十几分钟的 endurance 用例不该出现在任何卡合并的流水线里。
  4. Smoke 清单是受治理的资产。 要有耗时预算和明确 Owner,不然迟早膨胀成不是 smoke 的样子。
  5. 绝对不能让 E2E 意外变更生产数据。 关闭动态创建、静态账号、禁止 admin 调用到生产环境。
  6. 命令行参数优先级高于 .envdotenv override: false)。"跑错了环境"的静默 Bug 极难排查。
  7. Skip 必须显式、门控、加注释说明后续移除计划。 静默 skip 就是穿着绿勾外衣的丢失覆盖。