自动化流水线提效·微观篇:pytest 执行顺序优化

在上一篇流水线提效中我们提到:"用例执行顺序优化"会单独出文章讲解,本文就来填这个坑。当用例数量上千、并发数开到 8 时,经常会遇到一个反直觉现象:并发越高,反而越慢,通过率也变低。根因不在并发本身,而在"用例执行顺序不合理"导致的环境资源争抢。本文从问题定位、方案演进到最终代码实现,一步步讲清如何用一段 20 行的 conftest 钩子把 pytest 用例顺序打散到"文件级"。


一、问题现象:并发越高反而越慢

某项目用例规模 ~2000 条,pytest-xdist 8 进程并发。现象:

现象 表现
CPU 利用率波动大 api 阶段 20%、compute 阶段 95%+ 反复横跳
创建云主机用例集中爆发 8 个 worker 同时打云平台 API,触发限流
用例整体耗时变长 单条平均耗时从 2s → 6s
通过率下降 95% → 82%,偶发超时增多

直观观察 Jenkins 日志:

复制代码
gw0: tests/api/test_login.py::test_login         ← 低负载
gw1: tests/api/test_user.py::test_create_user   ← 低负载
...
gw0: tests/compute/test_vm.py::test_create_vm   ← 高负载
gw1: tests/compute/test_vm.py::test_clone_vm    ← 高负载
gw2: tests/compute/test_disk.py::test_create_disk ← 高负载
... 8 个 worker 全在打 compute,云平台 API 限流

资源使用呈现"闲时很闲、忙时很忙"的脉冲式波动------并发根本没有平滑负载。


二、根因分析:两个准则 + 一个默认行为

自动化用例管理有两个不成文的准则:

准则 1:用例按功能模块分类存放

复制代码
tests/
├── api/                  ← 接口测试,低负载
│   ├── test_login.py
│   └── test_user.py
├── compute/              ← 云主机模块,高负载
│   ├── test_vm.py
│   └── test_disk.py
└── network/
    └── test_subnet.py

好处 :管理清晰、维护方便

隐患 :相同功能的用例聚在一起,负载特征也聚在一起

准则 2:pytest 默认按名称排序执行

pytest 收集完用例后,默认按 nodeid 字母顺序排序:

复制代码
tests/api/test_login.py::test_a
tests/api/test_login.py::test_b
tests/api/test_user.py::test_a
tests/compute/test_vm.py::test_a       ← 高负载集中在这里
tests/compute/test_vm.py::test_b
tests/compute/test_disk.py::test_a

问题叠加:并发 + 默认顺序

  • xdist 默认 --dist=load:按用例逐个分发给 worker

  • 但分发顺序就是默认顺序(按文件名)

  • 结果:所有 worker 同时从 api 转到 compute高负载用例在同一时间窗集中爆发

    时间 →
    gw0: api_a → api_b → vm_a → vm_b → disk_a
    gw1: api_a → api_b → vm_a → vm_b → disk_a
    gw2: api_a → api_b → vm_a → vm_b → disk_a

    所有 worker 同时进入高负载区

这就是"闲时很闲、忙时很忙"的根因


三、方案一:用例级随机打乱(不推荐)

第一反应:用现成插件打乱顺序即可:

插件 粒度 用法
pytest-randomly 用例级 pip install pytest-randomly
pytest-random-order 用例级 pytest --random-order

装上后跑一遍,结果更慢了。为什么?

反例分析

假设 tests/compute/test_vm.py 有 3 个用例:

python 复制代码
# tests/compute/test_vm.py
@pytest.fixture(scope="module")
def vm():
    """module 级 fixture:一次创建,全文件共享"""
    vm = create_vm()
    yield vm
    delete_vm(vm)

def test_a(vm): ...      # 共用 vm
def test_b(vm): ...      # 共用 vm
def test_c(vm): ...       # 共用 vm

单独跑文件:1 次 create_vm + 3 次测试 + 1 次 delete_vm = 5 次云平台调用。

用例级打乱后:3 个用例被分到不同位置执行,可能跨文件、跨 worker,fixture 复用被破坏:

复制代码
worker A: test_a(vm) → 创建 vm1 → 用 → 销毁
worker B: test_b(vm) → 创建 vm2 → 用 → 销毁
worker C: test_c(vm) → 创建 vm3 → 用 → 销毁

变成 3 次 create_vm + 3 次测试 + 3 次 delete_vm = 9 次云平台调用。

fixture 范围越大(module/session),用例级打乱的代价越大


四、方案二:文件级随机打乱(推荐)

正确的优化方向:打乱到文件级即可,文件内保持原顺序

复制代码
原顺序:                  文件级打乱后:
api/test_login.py        compute/test_vm.py     ← 高负载先来一个
api/test_user.py         api/test_login.py
compute/test_vm.py       network/test_subnet.py
compute/test_disk.py     compute/test_disk.py
network/test_subnet.py   api/test_user.py

好处

维度 收益
负载分布 高负载用例被打散到不同时间段,避免集中爆发
fixture 复用 文件内顺序不变,module/session fixture 仍然只跑一次
环境压力 云平台 API 请求被平滑到整个执行周期

用一句话总结

打散要恰到好处------足够打散负载峰值,但不要破坏 fixture 复用。


五、核心代码:20 行 conftest 搞定文件级打散

直接上代码,加在项目根目录 conftest.py

python 复制代码
# conftest.py
import random


def pytest_collection_modifyitems(session, config, items: list):
    """用于随机化打乱测试用例文件"""
    items_list = []
    alike = []
    for i in items:
        i.add_marker(i.name)
        if not alike or i.parent.parent.nodeid == alike[0].parent.parent.nodeid:
            alike.append(i)
        else:
            items_list.append(alike)
            alike = [i]
    if alike:
        items_list.append(alike)
    random.seed(80)
    random.shuffle(items_list)
    random.seed()
    items[:] = [item for group in items_list for item in group]

用法 :放到 conftest.py 即可,无需任何命令行参数。每次跑同一个种子(80)打出同样的顺序,可复现


六、代码逐段拆解

1. 钩子签名

python 复制代码
def pytest_collection_modifyitems(session, config, items: list):
  • pytest_collection_modifyitems:pytest 收集完用例后、开始执行前的钩子,可修改 items
  • items:所有用例对象列表,按默认(字母)顺序排列
  • 修改 items[:] 即可改变执行顺序

2. 按文件分组

python 复制代码
items_list = []
alike = []
for i in items:
    i.add_marker(i.name)
    if not alike or i.parent.parent.nodeid == alike[0].parent.parent.nodeid:
        alike.append(i)
    else:
        items_list.append(alike)
        alike = [i]
if alike:
    items_list.append(alike)

核心i.parent.parent.nodeid 是用例所在的目录 (粗粒度),相同目录的用例聚到一组 alike,碰到新目录就把当前组存起来、开启新组。

最终 items_list 是"目录块"的列表:

复制代码
items_list = [
    [api_test_a, api_test_b, api_test_c],     ← api 目录
    [compute_test_a, compute_test_b],          ← compute 目录
    [network_test_a],                          ← network 目录
]

注:用 i.parent.parent.nodeid(祖父目录)作为分组键。如果你的目录结构更深,可以调整为 i.parent.nodeid(父目录,即文件级)或更上层。

3. 随机打乱组

python 复制代码
random.seed(80)
random.shuffle(items_list)
random.seed()
  • random.seed(80):固定种子,保证每次跑顺序一致(可复现 bug)
  • random.shuffle(items_list):打乱目录块的顺序,块内顺序保持不变
  • random.seed():重置种子,避免影响后续代码的随机数

4. 展平回 items

python 复制代码
items[:] = [item for group in items_list for item in group]

把"列表的列表"展平回一维列表,赋值给 items[:]------必须用 items[:] 而非 items =,前者是原地修改,后者只是局部变量赋值,对 pytest 不生效。

5. 顺带加 marker

python 复制代码
i.add_marker(i.name)

把每个用例的名字作为 marker 加上去,方便后续用 pytest -m test_a 单独跑某条用例。可选,删掉也不影响主逻辑。


七、效果验证

优化前

复制代码
worker 0: api_a → api_b → vm_a → vm_b → disk_a
worker 1: api_a → api_b → vm_a → vm_b → disk_a
...

CPU 使用率:  ▁▁▂▃▆█████▆▃▂▁▁  (脉冲式)
云平台 QPS:  ▁▁▁▁▁███▇▆▃▁▁▁  (集中爆发)
通过率:       82%
总耗时:       24min

优化后

复制代码
worker 0: vm_a → api_a → subnet_a → disk_a → api_b
worker 1: api_b → vm_b → disk_b → api_a → subnet_b
...

CPU 使用率:  ▃▄▅▄▅▆▅▄▅▆▅▄▅  (平滑)
云平台 QPS:  ▃▄▃▄▃▄▃▄▃▄▃▄▃  (均匀分布)
通过率:       94%
总耗时:       14min

关键指标改善

指标 优化前 优化后 提升
总耗时 24min 14min -42%
通过率 82% 94% +12%
CPU 利用率方差 0.32 0.08 平滑 4x
云平台 API 限流次数 17 0 消除

八、常见坑点速查

现象 解决
items = 而非 items[:] 顺序没变 必须原地修改
种子不固定 每次顺序不同,bug 不可复现 random.seed(80) 固定
分组粒度选错 打散效果差或破坏 fixture 按目录结构选 parentparent.parent
pytest-randomly 共存 两个随机冲突 二选一,本文方案与 randomly 互斥
xdist --dist=loadscope 冲突 文件级打散被 scope 收回 配合 --dist=load
同文件用例依赖顺序 打乱后失败 本就不该有顺序依赖,去掉依赖或加 marker 控
session fixture 仍是热点 多 worker 同时进入 fixture fixture 内部要幂等或加锁

九、扩展思路

1. 按 marker 加权打散

让"高负载 marker"之间留间隔:

python 复制代码
def pytest_collection_modifyitems(session, config, items):
    # 把高负载用例按固定间隔插入
    heavy = [i for i in items if any(m.name == "heavy" for m in i.iter_markers())]
    light = [i for i in items if i not in heavy]
    random.seed(42)
    random.shuffle(light)
    random.shuffle(heavy)
    # 每隔 N 个 light 插一个 heavy
    result = []
    heavy_idx = 0
    for idx, item in enumerate(light):
        result.append(item)
        if idx % 5 == 0 and heavy_idx < len(heavy):
            result.append(heavy[heavy_idx])
            heavy_idx += 1
    items[:] = result

2. 按历史耗时排序

记录每条用例的历史耗时,长的用例先跑------早暴露问题:

python 复制代码
def pytest_collection_modifyitems(session, config, items):
    durations = session.config.cache.get("case_durations", {})
    items.sort(key=lambda i: -durations.get(i.nodeid, 0))

配合 pytest --cache 用。

3. 配合 --dist=loadgroup 精细控制

python 复制代码
@pytest.mark.group("heavy-vm")
def test_create_vm(): ...
bash 复制代码
pytest --dist=loadgroup

让带 heavy-vm marker 的用例集中到一个 worker,避免并发打云平台。

4. 失败用例优先重跑

python 复制代码
def pytest_collection_modifyitems(session, config, items):
    failed = session.config.cache.get("lastfailed", [])
    items.sort(key=lambda i: i.nodeid in failed, reverse=True)

上次失败的用例排前面,早跑早暴露。

5. 同目录但想拆开

如果某目录用例太多,单纯按目录打散不够,可以按"文件 + 序号奇偶"再分:

python 复制代码
for i in items:
    group_key = (i.parent.nodeid, hash(i.name) % 2)

十、总结

pytest 用例执行顺序优化的核心思想:打散负载峰值,但保留 fixture 复用。掌握本文需要抓住三条主线:

  1. 根因:默认字母排序 + 同功能聚集 = 负载脉冲
  2. 粒度:用例级打散破坏 fixture,文件级/目录级打散刚刚好
  3. 可复现:用固定随机种子保证每次跑顺序一致,bug 可复现

一句话记法

用例顺序问题,不在"打不打",而在"打多碎"------文件级打散是性价比最高的平衡点。

实践建议

  1. 第一步 :用 pytest --durations=10 找出最慢的 10 个用例,看是不是同一目录
  2. 第二步:加上本文 20 行 conftest,对比耗时与通过率
  3. 第三步 :根据实际目录结构调整分组粒度(parent vs parent.parent
  4. 第四步 :把高负载用例显式打 heavy marker,配合 loadgroup 进一步精细化

把执行顺序打散做对后,并发才真正发挥威力------8 个 worker 才能稳定跑出 4-6 倍速,而不是脉冲式争抢资源。


建议动手实验:在项目根 conftest.py 加上本文 20 行代码,跑两次对比 pytest --durations=20 输出------直观看到高负载用例被打散到不同时间段。


相关推荐
童槿顏丶4 小时前
docker自动化部署脚本
docker·eureka·自动化
具身智能进化论6 小时前
协作机器人产业进入规模化部署期,未来几年的增长从何而来
大数据·运维·人工智能·机器人·自动化·工厂方法模式
微硬创新9 小时前
耐达讯自动化:16路4-20mA模拟信号,该怎样平稳接入PROFINET控制系统?
人工智能·物联网·网络协议·自动化·信息与通信
梦想的旅途210 小时前
企业微信API二次开发:接入 AI 大模型实现外部群智能问答
人工智能·自动化·二次开发·企业微信
lushanzheng11 小时前
2026年必应快排程序:基于 Selenium 自动化解决方案
selenium·测试工具·自动化
mengpp_12345611 小时前
物联网云平台可以离线使用吗?摒弃云端依赖,工业现场离线自动化解决方案
运维·物联网·自动化·华为云
xiaoxiangsiyan12 小时前
企业网络运维自动化与管理协议深度指南
linux·运维·服务器·网络·学习·自动化
梦想的旅途212 小时前
企业微信API二次开发:外部群自动化管理与消息回调实战指南
windows·自动化·企业微信
HackTwoHub12 小时前
Fastjson1全版本(1.2.47→1.2.83 )漏洞利用3万字完整总结、绕过分析、探测、WAF 绕过与 Gadget 汇总
前端·javascript·人工智能·安全·ajax·网络安全·自动化
酷可达拉斯13 小时前
自动化运维-从零开始编写ansible剧本(二)
linux·运维·服务器·centos·自动化