Skip to content

第 15 章 · 并行执行与 Sharding

本章目标:用 pytest-xdist 把 E2E 套件并行化,理解"隔离是并行的前提",并掌握 CI 中的分片与报告合并。

15.1 为什么 E2E 特别需要并行

Playwright 每条用例都要启动浏览器上下文,单进程串行时 200 条用例轻松跑掉半小时。官方文档给出的并行方案就是 pytest-xdist,推荐进程数取逻辑 CPU 核数的一半

bash
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 维度的隔离账号可以这样注入:

python
# 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):
python
@pytest.mark.xdist_group(name="checkout")   # 同组用例锁定同一 worker
class TestCheckoutFlow:
    def test_add_to_cart(self, page): ...
    def test_pay(self, page): ...
bash
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 配合环境变量做分片选择:

yaml
# .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 并行下的失败定位流程

并行失败的定位三步法:

  1. 单独复现pytest tests/test_x.py::test_y 串行跑一遍——若通过,基本可断定是共享状态冲突而非功能 bug;
  2. 锁定冲突方-n 2 --dist loadfile 缩小范围,或临时把可疑模块标记到同一 xdist_group 观察是否恢复;
  3. 留证据:本地复现时开 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(多机分片),下列做法正确的是?

🛠️ 动手实践

  1. 给你的套件接入 -n auto,记录串行与并行的总耗时对比。
  2. 实现 15.2 的 worker_account fixture,让两条同时登录的用例不再互踢会话。
  3. 故意制造一个共享数据冲突(两个用例操作同一条商品记录),观察并行下的偶发失败,再用 API 造数改造为隔离版本。

完成后进入下一章:CI 集成与 Docker