news 2026/9/17 11:08:08

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

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
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 机器上并行,每台机器运行--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中通过fullyParallelworkers两个字段控制单机并发行为(原文档完整示例):

// 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 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 }}

这段生产配置完美印证了原文档的三大要点:

  1. 分片与 project 正交组合:chromium 与 firefox 各有 4 片,共 8 个并发任务;PWTEST_BLOB_REPORT_NAME让每个分片写出唯一命名的 blob reportblob-report/chromium-1.zip之类),这是后续合并不出冲突的前提。
  2. fail-fast: false必须显式设置:任一 shard 失败不会取消其余 7 个任务(对应原文档反模式表的最后一条)。
  3. 上传命名带分片索引的产物(第 212-219 行):playwright-report-${{ matrix.project }}-${{ matrix.shardIndex }},同时把e2e/blob-reporte2e/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-reports

GitHub 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-artifactpattern: 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,而是挂了仓库自研的自定义 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-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、各配多少"。

场景 → 配置建议

ScenarioWorkersShardsReason
< 50 tests, < 5 minAuto (default)NoneNo optimization needed
50-200 tests, 5-15 min'50%'in CI2-4Balance speed and cost
200+ tests, > 15 min'50%'in CI4-8Keep feedback under 10 min
Flaky due to resource contentionReduce to 2KeepLess CPU/memory pressure
Tests modify shared database1 or isolateUsefulSharding splits files; workers run them
CI has limited resources1 or'25%'MoreCompensate with more machines

Workers 与 Shards 的本质区别

AspectWorkers (in-process)Shards (across machines)
What it splitsTests across CPU coresTest files across CI jobs
Controlled byConfig or--workersCLI--shard=X/YCLI flag
Shares memoryYesNo
Report mergingNot neededRequired (merge-reports)
CostFree (same machine)More CI minutes

读表结论:worker 不花钱(同一台机器),shard 花 CI 分钟;分片后报告合并是硬性要求;测试操作共享数据库时,要么把 workers 降到 1,要么用分片隔离文件——因为分片按文件切、天然减少了跨文件共享面的竞争。

反模式清单

原文档整理了一张反模式表,每条都是真实踩坑经验:

Anti-PatternProblemSolution
fullyParallel: falsewithout reasonTests in files run seriallySetfullyParallel: trueunless tests need serial
workers: 1in CI "for safety"Negates parallelismFix isolation issues; useworkers: '50%'
Hardcoded shared user accountRace conditions in parallel runsEach test creates unique data
Sharding without blob reporterEach shard produces separate HTML reportConfigurereporter: [['blob']]for CI
Sharding with 3 testsSetup overhead exceeds time savedOnly shard when suite > 5 minutes
test.describe.serial()everywhereKills parallelism, creates dependenciesUse only when tests genuinely need prior state
Workers > CPU coresContext switching overheadUse'50%'or auto-detect
Missingfail-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: 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-reports

    Sanity 仓库正是如此:上传名带${{ 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),仅供参考

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

Hister 全文索引完全揭秘:语言分析器与倒排索引原理

Hister 全文索引完全揭秘&#xff1a;语言分析器与倒排索引原理 【免费下载链接】hister Your own search engine 项目地址: https://gitcode.com/GitHub_Trending/hi/hister Hister 是一个自托管的全文搜索引擎&#xff1a;它把访问过的网页和本地文件的内容完整存下来…

作者头像 李华
网站建设 2026/9/17 11:07:10

液晶屏选型与驱动实战:从段码屏到TFT的完整避坑指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/17 11:06:59

d3dx9_26.dll缺失:DirectX旧组件与运行库排查指南

周末从柜子里翻出一张十几年前的老游戏光盘&#xff0c;装完、双击图标&#xff0c;屏幕正中弹出一行小字&#xff1a;找不到 d3dx9_26.dll。这个提示我前后遇到过不下几十次&#xff0c;从 Windows XP 时代一直到现在的 Windows 11&#xff0c;它出现的姿势几乎没变过。很多人…

作者头像 李华
网站建设 2026/9/17 11:06:28

VW 60330 无焊压接标准:尺寸链、切片与压接力监控

简介&#xff1a;这是一份面向汽车电子、线束制造及质量检测从业者的VW 60330中文版技术标准文档&#xff0c;聚焦无焊压接连接的技术规范与试验方法。压接连接广泛应用于汽车、航空、医疗设备等领域的电气信号传输&#xff0c;该文档系统梳理了开口式压接管、闭口式压接管、导…

作者头像 李华
网站建设 2026/9/17 11:06:19

Windows运维必备:bat脚本中reg命令注册表操作全指南

注册表这东西&#xff0c;很多人平时不愿碰&#xff0c;觉得它像Windows的“黑匣子”&#xff0c;改错一个键就可能让系统闹脾气。但只要你做Windows运维、桌面支持、批量部署&#xff0c;或者只是想让自己的机器少点重复点击&#xff0c;迟早会撞上bat脚本加注册表这个组合。而…

作者头像 李华