Sanity 仓库 Playwright 测试并行化与分片(Sharding)执行实战指南
【免费下载链接】sanitySanity Studio – Rapidly configure content workspaces powered by structured content项目地址: https://gitcode.com/GitHub_Trending/sa/sanity
本文是仓库内 Playwright 最佳实践技能文档(
.agents/skills/playwright-best-practices/infrastructure-ci-cd/parallel-sharding.md)的完整展开:它系统讲解如何用workers在单机上并发跑测试、如何用shard把测试拆分到多个 CI 任务上,以及如何用merge-reports把分片产物合并成统一报告。文中所有结论都以 Sanity Studio 仓库中真实的 E2E 配置(e2e/playwright.config.ts)与 CI 工作流(.github/workflows/e2e.yml)为佐证。读完你既能照抄命令行配置自己的流水线,也能理解"什么时候该加 worker、什么时候该加分片、为什么fail-fast: false必不可少"背后的原理。
何时使用并行执行与分片
Playwright 提供了两种互不排斥的加速手段,对应的使用场景不同:
- 单机内并行(workers):在同一台机器上并发执行测试,用于榨干多核 CPU。当套件规模适中(例如 50–200 个测试)时,通过调大 workers 即可明显缩短耗时。
- 跨机器分片(sharding):把测试按文件拆分到多台 CI 机器上并行,每台机器运行
--shard=X/Y指定的第 X/Y 片。当套件即便用满单机 workers 仍超过约 5 分钟时,就该考虑分片。
技能文档给出的判据是:套件超过 5 分钟且 worker 已拉满,才值得引入分片;规模很小(如不足 50 个测试、总耗时 < 5 分钟)时,默认配置就是最优解,不要为分片而分片。
CLI 命令速查
原文档给出的核心命令如下,它们分别对应"单机并行、跨机分片、合并报告"三类操作:
# Parallelism within one machine(单机内并行) npx playwright test --workers=4 npx playwright test --workers=50% # Splitting across CI jobs(跨 CI 任务分片) npx playwright test --shard=1/4 npx playwright test --shard=2/4 # Merging shard outputs(合并分片产物) npx playwright merge-reports ./blob-report npx playwright merge-reports --reporter=html,json ./blob-report # Override config for single run(单次运行覆盖配置) npx playwright test --fully-parallel要点说明:
--workers接受数字(固定数量)或百分比字符串(按 CPU 核数比例计算,如50%)。--shard=1/4表示把测试文件平均切成 4 份、运行第 1 份;分片粒度是测试文件,因此分片数不能超过文件数(否则会出现空分片,见"故障排查")。merge-reports是分片流水线的收尾动作:把各分片生成的blob report合并为一份完整 HTML/JSON 报告。
单机并行:Worker 配置
在playwright.config.ts中通过fullyParallel与workers两个字段控制单机并发行为(原文档完整示例):
// playwright.config.ts import {defineConfig} from '@playwright/test' export default defineConfig({ // Tests WITHIN a file also run in parallel(文件内用例也并行) fullyParallel: true, // Worker count options: // - undefined: auto-detect (half CPU cores)(自动检测:核数的一半) // - number: fixed count(固定数量) // - string: percentage of cores(按核数百分比) workers: process.env.CI ? '50%' : undefined, })fullyParallel的行为矩阵(原文档表格):
| Setting | Files parallel | Tests in file parallel |
|---|---|---|
fullyParallel: false(默认) | 是 | 否(文件内串行) |
fullyParallel: true | 是 | 是 |
也就是说,默认配置下不同文件之间并行、同一文件内的用例串行;开启fullyParallel后文件内用例也并发。这种"文件内串行"是很多偶发超时的来源——如果文件内用例互不依赖,务必显式开启。
对特定文件强制串行:当某个文件内的用例确实有先后依赖(如购物流程"先加购、再支付"),可用test.describe.configure单独声明:
// tests/checkout-flow.spec.ts import {test, expect} from '@playwright/test' test.describe.configure({mode: 'serial'}) test('add items to cart', async ({page}) => { // ... }) test('complete payment', async ({page}) => { // ... })Sanity 仓库的落地方案:在e2e/playwright.config.ts中,Sanity 的 E2E 套件正是fullyParallel: true,并搭配retries: 2(失败自动重试 2 次)、reporter: excludeGithub([['list'], ['blob']])——这里就已经为分片合并埋下了blob reporter的伏笔(见下文"合并分片报告")。excludeGithub是一个仓库内的工具函数,用于在 GitHub PR 噪音问题(microsoft/playwright#19817)解决前剔除githubreporter。
跨机器分片:Sharding Across CI Machines
原文档给出的判据:即使把 workers 开到最大、套件总耗时仍超过 5 分钟,就应把测试拆到多台 CI 机器上。典型形态是 4 台机器各自运行一片:
# Job 1 Job 2 Job 3 Job 4 --shard=1/4 --shard=2/4 --shard=3/4 --shard=4/4面向分片运行的配置(原文档完整示例):
// playwright.config.ts import {defineConfig} from '@playwright/test' export default defineConfig({ fullyParallel: true, workers: process.env.CI ? '50%' : undefined, // CI 上必须使用 blob reporter,才能让各分片产物最终被 merge-reports 合并 reporter: process.env.CI ? [['blob'], ['github']] : [['html', {open: 'on-failure'}]], })Sanity 仓库的真实分片矩阵(.github/workflows/e2e.yml):E2E 工作流在playwright-testjob 中同时按浏览器 × 分片展开矩阵:
strategy: fail-fast: false matrix: project: [chromium, firefox] shardIndex: [1, 2, 3, 4] shardTotal: [4]每个矩阵任务执行(对应文件第 205-210 行):
- name: Run E2E tests env: PWTEST_BLOB_REPORT_NAME: ${{ matrix.project }} run: pnpm test:e2e --project ${{ matrix.project }} --shard ${{ matrix.shardIndex }}/${{ matrix.shardTotal }}这段生产配置完美印证了原文档的三大要点:
- 分片与 project 正交组合:chromium 与 firefox 各有 4 片,共 8 个并发任务;
PWTEST_BLOB_REPORT_NAME让每个分片写出唯一命名的 blob report(blob-report/chromium-1.zip之类),这是后续合并不出冲突的前提。 fail-fast: false必须显式设置:任一 shard 失败不会取消其余 7 个任务(对应原文档反模式表的最后一条)。- 上传命名带分片索引的产物(第 212-219 行):
playwright-report-${{ matrix.project }}-${{ matrix.shardIndex }},同时把e2e/blob-report与e2e/results一并上传,保留期 30 天。
分片还带来一个副作用:每次失败重试都会产生大量视频/追踪产物。Sanity 仓库额外做了一个诊断抽取步骤(第 223-233 行pnpm --filter e2e extract-diagnostics),把失败尝试的 Studio 诊断数据压缩成e2e-diagnostics-*小产物(保留 90 天),避免流水线被大 artifact 拖垮——实现见e2e/scripts/flakeReport/extractDiagnostics.ts。
合并分片报告:merge-reports
分片后每个 job 各自产出 blob report,必须合并才能得到完整视图。原文档给出的命令:
# Merge all blobs into HTML npx playwright merge-reports --reporter=html ./all-blob-reports # Multiple formats npx playwright merge-reports --reporter=html,json,junit ./all-blob-reports # Custom output location PLAYWRIGHT_HTML_REPORT=merged-report npx playwright merge-reports --reporter=html ./all-blob-reportsGitHub Actions 合并 job 的标准模板(原文档完整示例):
merge-reports: if: ${{ !cancelled() }} needs: test runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - run: npm ci - uses: actions/download-artifact@v4 with: path: all-blob-reports pattern: blob-report-* merge-multiple: true - run: npx playwright merge-reports --reporter=html ./all-blob-reports - uses: actions/upload-artifact@v4 with: name: playwright-report path: playwright-report/ retention-days: 14关键点:download-artifact用pattern: blob-report-*+merge-multiple: true把全部 shard 产物拉进同一目录;merge-reports的输出默认落在playwright-report/。
Sanity 仓库的合并 job 更进了一步(.github/workflows/e2e.yml):
- name: Download blob reports from Github Actions Artifacts uses: actions/download-artifact@v8 with: pattern: playwright-report-* merge-multiple: true path: all-blob-reports - name: Merge into HTML Report run: pnpm exec playwright merge-reports --reporter html,./e2e/reporters/summary.ts all-blob-reports/blob-report它没有用内置的html,json,junit,而是挂了仓库自研的自定义 reporter(e2e/reporters/summary.ts)。该文件注释明确写明了用途与用法:
Writes two files (used by the CI
merge-reports/deploy-reportjobs):npx playwright merge-reports --reporter html,./e2e/reporters/summary.ts blob-reports
summary.ts在合并时生成test-summary.json(输出 passed/failed/flaky/skipped 等计数,供 e2e.yml 第 336-344 行解析后写进 PR 评论)以及agent-report.md——一份纯 Markdown 的失败摘要。如e2e/README.md所述,HTML 报告是 JS 单页应用,"对人类友好,但 AI Agent 抓取 URL 只能拿到空壳",而agent-report.md是合并阶段生成的纯文本摘要,专门供 Agent/LLM 消费。这正是"分片 + 合并"流水线在真实大规模仓库中的进阶形态。
此外,e2e/scripts/flakeReport/blobReport.ts展示了 blob report 的内部结构:每个 blob 是一个 zip,内部含report.jsonl(v2 jsonl 格式),flake-report 工具就是解压这些 zip 来聚合历史分片数据的。
Worker 级 Fixture:昂贵资源只建一次
分片/并行会放大"每个测试都去建连接"的开销。原文档给出worker 级 fixture模式:数据库连接、认证 token 这类昂贵资源,应在每个 worker 内只创建一次,而不是每个测试各建一次:
// fixtures.ts import {test as base} from '@playwright/test' type WorkerFixtures = { dbClient: DatabaseClient apiToken: string } export const test = base.extend<{}, WorkerFixtures>({ dbClient: [ async ({}, use) => { const client = await DatabaseClient.connect(process.env.DB_URL!) await use(client) await client.disconnect() }, {scope: 'worker'}, // 关键:声明为 worker 作用域 ], apiToken: [ async ({}, use, workerInfo) => { const res = await fetch(`${process.env.API_URL}/auth`, { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({ user: `test-user-${workerInfo.workerIndex}`, // workerIndex 保证不同 worker 用户唯一 password: process.env.TEST_PASSWORD, }), }) const {token} = await res.json() await use(token) }, {scope: 'worker'}, ], }) export {expect} from '@playwright/test'两个细节值得注意:
{scope: 'worker'}是 worker 级 fixture 的开关,缺了它资源就会退化为 test 级、失去复用价值(这也是"故障排查"一节专门列出的问题)。- 通过
workerInfo.workerIndex生成test-user-${workerInfo.workerIndex},天然规避了不同 worker 之间账号冲突——这同时是解决并行竞争的关键手法(见下一节)。
并行前提:测试隔离(Test Isolation)
并行/分片之所以容易"单跑全绿、合跑翻车",九成是因为测试共享了状态。原文档强调:每个测试必须自建状态,不得依赖或修改共享状态。
反例:共享用户导致竞态
// BAD: Shared user causes race conditions test('edit settings', async ({page}) => { await page.goto('/users/test-user/settings') await page.getByLabel('Email').fill('new@example.com') await page.getByRole('button', {name: 'Save'}).click() })两个 worker 同时编辑test-user的邮箱,互相覆盖,断言时各自看到对方的数据。
正例:每个测试建唯一用户
// GOOD: Unique user per test test('edit settings', async ({page, request}) => { const res = await request.post('/api/users', { data: {name: `user-${Date.now()}`, email: `${Date.now()}@test.com`}, }) const user = await res.json() await page.goto(`/users/${user.id}/settings`) await page.getByLabel('Email').fill('updated@example.com') await page.getByRole('button', {name: 'Save'}).click() await expect(page.getByLabel('Email')).toHaveValue('updated@example.com') await request.delete(`/api/users/${user.id}`) // 用后即清,避免污染下次运行 })用testInfo生成唯一标识(适合不便于走 API 建数据的场景):
import {test, expect} from '@playwright/test' test('submit order', async ({page}, testInfo) => { const orderId = `order-${testInfo.workerIndex}-${Date.now()}` await page.goto(`/orders/new?ref=${orderId}`) // ... })Sanity 仓库的做法与此完全同构:E2E 套件针对每个 PR 部署独立的 staging dataset,并且 chromium/firefox 各自使用独立的SANITY_E2E_DATASET(见 e2e.yml 第 209 行(matrix.project == 'chromium') && env.CHROMIUM_DATASET || env.FIREFOX_DATASET),在数据源头就把两个浏览器项目的写操作隔离开,避免并行写同一数据集产生脏数据。
动态分片数量:按测试数自动计算
分片数是固定写死好,还是按测试数量动态算好?原文档给出的动态方案:先用--list统计测试总数,再按"每 20 个测试 1 片、最少 1 片、最多 8 片"生成矩阵:
# .github/workflows/playwright.yml jobs: calculate-shards: runs-on: ubuntu-latest outputs: shard-count: ${{ steps.calc.outputs.count }} shard-matrix: ${{ steps.calc.outputs.matrix }} steps: - uses: actions/checkout@v4 - run: npm ci - id: calc run: | TEST_COUNT=$(npx playwright test --list --reporter=json 2>/dev/null | node -e " const data = require('fs').readFileSync('/dev/stdin', 'utf8'); const parsed = JSON.parse(data); console.log(parsed.suites?.reduce((acc, s) => acc + (s.specs?.length || 0), 0) || 0); ") # 1 shard per 20 tests, min 1, max 8 SHARDS=$(( (TEST_COUNT + 19) / 20 )) SHARDS=$(( SHARDS > 8 ? 8 : SHARDS )) SHARDS=$(( SHARDS < 1 ? 1 : SHARDS )) MATRIX="[" for i in $(seq 1 $SHARDS); do [ $i -gt 1 ] && MATRIX+="," MATRIX+="\"$i/$SHARDS\"" done MATRIX+="]" echo "count=$SHARDS" >> $GITHUB_OUTPUT echo "matrix=$MATRIX" >> $GITHUB_OUTPUT test: needs: calculate-shards runs-on: ubuntu-latest strategy: fail-fast: false matrix: shard: ${{ fromJson(needs.calculate-shards.outputs.shard-matrix) }} steps: - uses: actions/checkout@v4 - run: npm ci - run: npx playwright install --with-deps - run: npx playwright test --shard=${{ matrix.shard }}这套方案的工程价值在于:套件随迭代增长时不必手动改分片数,calculate-shardsjob 用--list --reporter=json拿到精确的测试数并推导出矩阵,testjob 通过fromJson展开矩阵。Sanity 仓库则选择了"固定 4 片 × 2 浏览器"的稳定矩阵(见前文 e2e.yml),两种策略各有利弊:动态方案省 CI 分钟、静态方案更可预期,可结合自身套件增速选择。
决策指南
原文档给出两张决策表,直接决定"该用 workers 还是 shards、各配多少"。
场景 → 配置建议:
| Scenario | Workers | Shards | Reason |
|---|---|---|---|
| < 50 tests, < 5 min | Auto (default) | None | No optimization needed |
| 50-200 tests, 5-15 min | '50%'in CI | 2-4 | Balance speed and cost |
| 200+ tests, > 15 min | '50%'in CI | 4-8 | Keep feedback under 10 min |
| Flaky due to resource contention | Reduce to 2 | Keep | Less CPU/memory pressure |
| Tests modify shared database | 1 or isolate | Useful | Sharding splits files; workers run them |
| CI has limited resources | 1 or'25%' | More | Compensate with more machines |
Workers 与 Shards 的本质区别:
| Aspect | Workers (in-process) | Shards (across machines) |
|---|---|---|
| What it splits | Tests across CPU cores | Test files across CI jobs |
| Controlled by | Config or--workersCLI | --shard=X/YCLI flag |
| Shares memory | Yes | No |
| Report merging | Not needed | Required (merge-reports) |
| Cost | Free (same machine) | More CI minutes |
读表结论:worker 不花钱(同一台机器),shard 花 CI 分钟;分片后报告合并是硬性要求;测试操作共享数据库时,要么把 workers 降到 1,要么用分片隔离文件——因为分片按文件切、天然减少了跨文件共享面的竞争。
反模式清单
原文档整理了一张反模式表,每条都是真实踩坑经验:
| Anti-Pattern | Problem | Solution |
|---|---|---|
fullyParallel: falsewithout reason | Tests in files run serially | SetfullyParallel: trueunless tests need serial |
workers: 1in CI "for safety" | Negates parallelism | Fix isolation issues; useworkers: '50%' |
| Hardcoded shared user account | Race conditions in parallel runs | Each test creates unique data |
| Sharding without blob reporter | Each shard produces separate HTML report | Configurereporter: [['blob']]for CI |
| Sharding with 3 tests | Setup overhead exceeds time saved | Only shard when suite > 5 minutes |
test.describe.serial()everywhere | Kills parallelism, creates dependencies | Use only when tests genuinely need prior state |
| Workers > CPU cores | Context switching overhead | Use'50%'or auto-detect |
Missingfail-fast: falsein CI matrix | One shard failure cancels others | Always setfail-fast: falsefor sharded strategies |
对照 Sanity 仓库,可以逐条验证这些反模式是如何被规避的:fullyParallel: true已开启(e2e/playwright.config.ts)、CI reporter 固定含blob(同文件第 104 行)、矩阵声明了fail-fast: false(e2e.yml)、retries: 2只重试失败用例而非整体串行。需要强调最隐蔽的一条:"为安全起见把 workers 设成 1" 是最昂贵的伪安全——它把并行度归零,正确做法是修好测试隔离而不是阉割并发。
故障排查
测试单独跑通过、合跑就失败
- 共享状态。让测试数据唯一化:
test('create item', async ({request}, ti) => { await request.post('/api/items', { data: {name: `Item-${ti.workerIndex}-${Date.now()}`}, }) })
某些分片报 "No tests found"
- 分片数超过了文件数。分片按文件切割,分片数不得多于测试文件数:
npx playwright test --shard=1/10 # ok if 10 files(10 个文件时没问题) npx playwright test --shard=1/20 # too many, some shards empty(分片过多,部分分片为空)
合并后的报告缺结果
- Blob 报告相互覆盖。每个分片必须用唯一命名上传,合并时用 pattern 通配拉取:
# Each shard(每个分片) - uses: actions/upload-artifact@v4 with: name: blob-report-${{ strategy.job-index }} path: blob-report/ # Merge step(合并步骤) - uses: actions/download-artifact@v4 with: pattern: blob-report-* merge-multiple: true path: all-blob-reportsSanity 仓库正是如此:上传名带
${{ matrix.project }}-${{ matrix.shardIndex }},下载用pattern: playwright-report-*+merge-multiple: true(e2e.yml),并在分片侧通过PWTEST_BLOB_REPORT_NAME让 blob 文件本身也唯一。
Worker 级 fixture 不生效
- 漏了
{ scope: 'worker' }。修复:export const test = base.extend({ resource: [ async ({}, use) => { const r = await Resource.create() await use(r) await r.destroy() }, {scope: 'worker'}, ], })
加更多 worker 反而更慢
- worker 数超过 CPU 核数导致资源抖动。在 CI 上限制数量:
export default defineConfig({ workers: process.env.CI ? 2 : undefined, })
小结:把并行与分片用对
回到本文的起点:并行化不是"把数字调大"那么简单,而是一套从**配置(workers/fullyParallel)、编排(shard 矩阵 + fail-fast: false)、合并(blob + merge-reports)、隔离(唯一数据 + worker 级 fixture)到监控(分片产物诊断)**的完整体系。Sanity 仓库的e2e/playwright.config.ts与.github/workflows/e2e.yml是一份可以直接借鉴的参考实现:固定 4 分片 × chromium/firefox 双浏览器矩阵、blob reporter + 自定义 summary reporter 的合并管线,以及基于 blob zip 内report.jsonl的 flake 聚合工具(e2e/scripts/flakeReport/blobReport.ts),都值得在搭建自己的 Playwright CI 时分部对照。本文对应的技能文档原文位于.agents/skills/playwright-best-practices/infrastructure-ci-cd/parallel-sharding.md,配套的 CI/CD 话题还可参阅同目录的.agents/skills/playwright-best-practices/infrastructure-ci-cd/ci-cd.md与.agents/skills/playwright-best-practices/infrastructure-ci-cd/reporting.md。
【免费下载链接】sanitySanity Studio – Rapidly configure content workspaces powered by structured content项目地址: https://gitcode.com/GitHub_Trending/sa/sanity
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考