Sanity 仓库 Playwright 测试并行化与分片(Sharding)执行实战指南

📅 发布时间:2026/9/17 11:08:14
Sanity 仓库 Playwright 测试并行化与分片(Sharding)执行实战指南
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 机器上并行每台机器运行--shardX/Y指定的第 X/Y 片。当套件即便用满单机 workers 仍超过约 5 分钟时就该考虑分片。技能文档给出的判据是套件超过 5 分钟且 worker 已拉满才值得引入分片规模很小如不足 50 个测试、总耗时 5 分钟时默认配置就是最优解不要为分片而分片。CLI 命令速查原文档给出的核心命令如下它们分别对应单机并行、跨机分片、合并报告三类操作# Parallelism within one machine单机内并行 npx playwright test --workers4 npx playwright test --workers50% # Splitting across CI jobs跨 CI 任务分片 npx playwright test --shard1/4 npx playwright test --shard2/4 # Merging shard outputs合并分片产物 npx playwright merge-reports ./blob-report npx playwright merge-reports --reporterhtml,json ./blob-report # Override config for single run单次运行覆盖配置 npx playwright test --fully-parallel要点说明--workers接受数字固定数量或百分比字符串按 CPU 核数比例计算如50%。--shard1/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的行为矩阵原文档表格SettingFiles parallelTests in file parallelfullyParallel: 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 --shard1/4 --shard2/4 --shard3/4 --shard4/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.ymlE2E 工作流在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 reportblob-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 --reporterhtml ./all-blob-reports # Multiple formats npx playwright merge-reports --reporterhtml,json,junit ./all-blob-reports # Custom output location PLAYWRIGHT_HTML_REPORTmerged-report npx playwright merge-reports --reporterhtml ./all-blob-reportsGitHub Actions 合并 job 的标准模板原文档完整示例merge-reports: if: ${{ !cancelled() }} needs: test runs-on: ubuntu-latest steps: - uses: actions/checkoutv4 - run: npm ci - uses: actions/download-artifactv4 with: path: all-blob-reports pattern: blob-report-* merge-multiple: true - run: npx playwright merge-reports --reporterhtml ./all-blob-reports - uses: actions/upload-artifactv4 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-artifactv8 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而是挂了仓库自研的自定义 reportere2e/reporters/summary.ts。该文件注释明确写明了用途与用法Writes two files (used by the CImerge-reports/deploy-reportjobs):npx playwright merge-reports --reporter html,./e2e/reporters/summary.ts blob-reportssummary.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.jsonlv2 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(newexample.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(updatedexample.com) await page.getByRole(button, {name: Save}).click() await expect(page.getByLabel(Email)).toHaveValue(updatedexample.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/checkoutv4 - run: npm ci - id: calc run: | TEST_COUNT$(npx playwright test --list --reporterjson 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/checkoutv4 - run: npm ci - run: npx playwright install --with-deps - run: npx playwright test --shard${{ matrix.shard }}这套方案的工程价值在于套件随迭代增长时不必手动改分片数calculate-shardsjob 用--list --reporterjson拿到精确的测试数并推导出矩阵testjob 通过fromJson展开矩阵。Sanity 仓库则选择了固定 4 片 × 2 浏览器的稳定矩阵见前文 e2e.yml两种策略各有利弊动态方案省 CI 分钟、静态方案更可预期可结合自身套件增速选择。决策指南原文档给出两张决策表直接决定该用 workers 还是 shards、各配多少。场景 → 配置建议ScenarioWorkersShardsReason 50 tests, 5 minAuto (default)NoneNo optimization needed50-200 tests, 5-15 min50%in CI2-4Balance speed and cost200 tests, 15 min50%in CI4-8Keep feedback under 10 minFlaky due to resource contentionReduce to 2KeepLess CPU/memory pressureTests modify shared database1 or isolateUsefulSharding splits files; workers run themCI has limited resources1 or25%MoreCompensate with more machinesWorkers 与 Shards 的本质区别AspectWorkers (in-process)Shards (across machines)What it splitsTests across CPU coresTest files across CI jobsControlled byConfig or--workersCLI--shardX/YCLI flagShares memoryYesNoReport mergingNot neededRequired (merge-reports)CostFree (same machine)More CI minutes读表结论worker 不花钱同一台机器shard 花 CI 分钟分片后报告合并是硬性要求测试操作共享数据库时要么把 workers 降到 1要么用分片隔离文件——因为分片按文件切、天然减少了跨文件共享面的竞争。反模式清单原文档整理了一张反模式表每条都是真实踩坑经验Anti-PatternProblemSolutionfullyParallel: falsewithout reasonTests in files run seriallySetfullyParallel: trueunless tests need serialworkers: 1in CI for safetyNegates parallelismFix isolation issues; useworkers: 50%Hardcoded shared user accountRace conditions in parallel runsEach test creates unique dataSharding without blob reporterEach shard produces separate HTML reportConfigurereporter: [[blob]]for CISharding with 3 testsSetup overhead exceeds time savedOnly shard when suite 5 minutestest.describe.serial()everywhereKills parallelism, creates dependenciesUse only when tests genuinely need prior stateWorkers CPU coresContext switching overheadUse50%or auto-detectMissingfail-fast: falsein CI matrixOne shard failure cancels othersAlways setfail-fast: falsefor sharded strategies对照 Sanity 仓库可以逐条验证这些反模式是如何被规避的fullyParallel: true已开启e2e/playwright.config.ts、CI reporter 固定含blob同文件第 104 行、矩阵声明了fail-fast: falsee2e.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 --shard1/10 # ok if 10 files10 个文件时没问题 npx playwright test --shard1/20 # too many, some shards empty分片过多部分分片为空合并后的报告缺结果Blob 报告相互覆盖。每个分片必须用唯一命名上传合并时用 pattern 通配拉取# Each shard每个分片 - uses: actions/upload-artifactv4 with: name: blob-report-${{ strategy.job-index }} path: blob-report/ # Merge step合并步骤 - uses: actions/download-artifactv4 with: pattern: blob-report-* merge-multiple: true path: all-blob-reportsSanity 仓库正是如此上传名带${{ matrix.project }}-${{ matrix.shardIndex }}下载用pattern: playwright-report-*merge-multiple: truee2e.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),仅供参考