第 15 章 · 并行执行与 Sharding
本章目标:用 pytest-xdist 把 E2E 套件并行化,理解"隔离是并行的前提",并掌握 CI 中的分片与报告合并。
15.1 为什么 E2E 特别需要并行
Playwright 每条用例都要启动浏览器上下文,单进程串行时 200 条用例轻松跑掉半小时。官方文档给出的并行方案就是 pytest-xdist,推荐进程数取逻辑 CPU 核数的一半:
pip install pytest-xdist
pytest --numprocesses 4 # 官方推荐的并行开关
pytest -n auto # xdist 简写:自动按 CPU 数分配每个 worker 是独立进程、独立的 Playwright 驱动和浏览器实例——这既是速度来源,也是一切坑的根源:任何跨用例共享的可变状态在并行下都会变成偶发失败。
15.2 隔离是并行的前提
pytest-playwright 默认为每条测试创建全新 context + page(浏览器实例按 scope 复用),所以浏览器层天然隔离;真正的冲突来自被测系统:
| 冲突源 | 症状 | 解法 |
|---|---|---|
| 同一账号并发登录 | 会话互踢、购物车串数据 | 每个 worker 用独立测试账号(见下方 fixture) |
| 共享数据库记录 | A 用例删了 B 正在校验的数据 | 用例自建自删数据(API 造数,见第 17 章),或按 worker 分库/schema |
| 固定端口/固定文件路径 | Address already in use | 端口随机化、tmp_path 写临时文件 |
| 全局计数器/排行榜类断言 | 断言"第一名"但其他 worker 在写入 | 改断言为相对性(自己创建的记录可见),避开全局态 |
worker 维度的隔离账号可以这样注入:
# conftest.py —— 每个 xdist worker 拿到不同账号
import pytest
WORKER_ACCOUNTS = ["user0@t.io", "user1@t.io", "user2@t.io", "user3@t.io"]
@pytest.fixture(scope="session")
def worker_account(worker_id: str) -> str:
# worker_id 为 "master"(串行)或 gw0/gw1/gw2...
idx = 0 if worker_id == "master" else int(worker_id[2:])
return WORKER_ACCOUNTS[idx % len(WORKER_ACCOUNTS)]15.3 分发粒度与顺序的坑
xdist 默认按用例把收集到的测试分发到各 worker,这意味着两点必须心里有数:
- 执行顺序不可依赖。用例间若有先后依赖(B 的数据是 A 建的),并行下必然随机挂——正确做法不是调顺序,而是让每条用例自给自足;
- 若确需"同组用例固定在同一 worker"(如共享一次昂贵的登录),用
--dist loadscope(按模块/类分组分发)或loadgroup(配合@pytest.mark.xdist_group):
@pytest.mark.xdist_group(name="checkout") # 同组用例锁定同一 worker
class TestCheckoutFlow:
def test_add_to_cart(self, page): ...
def test_pay(self, page): ...pytest -n 4 --dist loadgroup另外注意:-s 输出与 print 日志在各 worker 交错,排查时加 -o log_cli=true 或直接看失败用例自己的 trace(第 11 章)更高效。
15.4 CI 中的 Sharding 与报告合并
单机并行有上限,大套件在 CI 里常用多机分片:每台机器只跑一部分,最后合并结果。纯 pytest 生态没有 Node 版 --shard 的内置等价物,常见做法是用 xdist 的 --dist loadfile 配合环境变量做分片选择:
# .github/workflows/e2e.yml 关键片段
strategy:
matrix:
shard: [1, 2, 3]
steps:
- name: Run shard
run: |
pip install pytest pytest-playwright pytest-xdist pytest-md-report
# 按测试文件名哈希决定本片跑哪些文件(简单可靠的静态分片)
python scripts/pick_shard.py --total 3 --index ${{ matrix.shard }} > files.txt
pytest $(cat files.txt) --numprocesses 4 \
--tracing=retain-on-failure
- name: Upload traces
if: ${{ !cancelled() }}
uses: actions/upload-artifact@v5
with:
name: traces-shard-${{ matrix.shard }}
path: test-results/要点:
- 分片键要稳定(按文件名而非运行时顺序),保证重跑同一片覆盖同样内容;
- 失败工件(traces/截图)按分片命名上传,合并查看;
- JUnit XML 报告(
--junitxml=report-{shard}.xml)可在 CI 层聚合展示总通过率。
经验值
先用 -n auto 吃满单机;当单机时间超过 10 分钟再引入矩阵分片,避免过早增加基础设施复杂度。
15.5 并行下的失败定位流程
并行失败的定位三步法:
- 单独复现:
pytest tests/test_x.py::test_y串行跑一遍——若通过,基本可断定是共享状态冲突而非功能 bug; - 锁定冲突方:
-n 2 --dist loadfile缩小范围,或临时把可疑模块标记到同一xdist_group观察是否恢复; - 留证据:本地复现时开
pytest --tracing=retain-on-failure,用 trace viewer 对比两次执行的请求时序。
15.6 本章小结
pytest --numprocesses N(官方)或-n auto开启并行,进程数取一半 CPU 核;- 浏览器上下文天然隔离,冲突几乎都来自共享账号/数据/端口,用 worker 维度账号与自建自删数据解决;
- xdist 按用例乱序分发,顺序依赖必须消灭;同组用例用
loadscope/loadgroup绑定; - CI 分片要按键稳定切分、按片上传工件、以 JUnit 报告聚合结果。
🧪 随堂测验
点击你认为正确的选项。答错时会展示正确答案与原因解析。
1. Playwright 官方推荐的 pytest 并行开启方式是?
2. 并行后某条用例"单独跑必过、并行跑偶发挂",最可能的原因是?
3. xdist 默认的分发策略意味着什么?
4. 关于 CI 中的 Sharding(多机分片),下列做法正确的是?
🛠️ 动手实践
- 给你的套件接入
-n auto,记录串行与并行的总耗时对比。 - 实现 15.2 的
worker_accountfixture,让两条同时登录的用例不再互踢会话。 - 故意制造一个共享数据冲突(两个用例操作同一条商品记录),观察并行下的偶发失败,再用 API 造数改造为隔离版本。
完成后进入下一章:CI 集成与 Docker。