用 pytest-randomly 打乱测试顺序,暴露全局状态依赖并用 fixture 隔离
用 pytest-randomly 打乱测试顺序,暴露全局状态依赖
问题场景
很多团队遇到过类似现象:单个测试文件运行通过,全量运行却失败;或者把两个测试函数调换顺序,结果一个通过、一个报错。这类问题通常不是业务代码本身有随机性,而是测试之间共享了可变全局状态。
pytest 默认按文件、类、函数的定义顺序执行测试。稳定的顺序会把顺序依赖隐藏起来,直到 CI 中调整了文件发现顺序、新增了测试,或者并行执行方式变化,问题才暴露。pytest-randomly 是一个第三方 pytest 插件,它会在每次会话中打乱测试项的执行顺序,并在输出中给出本次使用的随机种子,方便复现失败顺序。
最小可运行示例
创建 test_order_demo.py:
REGISTRY = {}
def test_register_user():
REGISTRY['user_id'] = 1001
assert REGISTRY['user_id'] == 1001
def test_read_user():
assert REGISTRY['user_id'] == 1001
安装并运行:
python -m pip install pytest pytest-randomly
pytest -q
pytest -q -p no:randomly
pytest -q -p no:randomly 会禁用该插件,便于对比默认顺序。启用 pytest-randomly 后,执行顺序会被打乱,并在输出顶部打印类似 Using --randomly-seed=<seed> 的信息。如果随机顺序先执行 test_read_user,它会因为 REGISTRY 中还没有 user_id 而抛出 KeyError。
复现某次失败顺序:
pytest -q --randomly-seed=<上次输出的种子>
也可以单独运行可疑测试,确认它对前置状态的依赖:
pytest -q test_order_demo.py::test_read_user
用 fixture 隔离状态
修复目标不是让随机顺序碰巧通过,而是让每个测试不依赖其他测试留下的状态。最直接的做法是把可变状态放进 fixture,让每个测试拿到独立实例。
import pytest
@pytest.fixture
def registry():
return {}
def test_register_user(registry):
registry['user_id'] = 1001
assert registry['user_id'] == 1001
def test_read_user(registry):
assert 'user_id' not in registry
registry['user_id'] = 2002
assert registry['user_id'] == 2002
pytest 默认的 fixture 作用域是 function,所以每个测试函数都会调用一次 registry,得到全新的字典。测试之间不再通过模块级变量传递状态。
如果历史代码已经大量使用模块级全局对象,可以先用 autouse fixture 做最小改动,在每个测试前后清理:
import pytest
REGISTRY = {}
@pytest.fixture(autouse=True)
def clean_registry():
REGISTRY.clear()
yield
REGISTRY.clear()
def test_register_user():
REGISTRY['user_id'] = 1001
assert REGISTRY['user_id'] == 1001
def test_read_user():
assert 'user_id' not in REGISTRY
REGISTRY['user_id'] = 2002
assert REGISTRY['user_id'] == 2002
autouse=True 让每个测试自动使用该 fixture。yield 之前是 setup,之后是 teardown;即使测试失败,teardown 也会执行。这样无论测试顺序如何,REGISTRY 都不会跨测试残留数据。
运行验证:
pytest -q
pytest -q --randomly-seed=<seed>
修复后,默认顺序和随机顺序都应稳定通过。
边界与限制
第一,pytest-randomly 只负责暴露顺序依赖,不负责修复。某次随机运行通过,不代表测试之间没有共享状态,可能只是没有触发冲突。
第二,随机化效果会受 pytest 配置、其他插件、pytest-xdist 并行执行方式影响。并行时每个 worker 有独立进程,某些共享状态问题可能被进程隔离掩盖,需要单独设计验证方式。
第三,随机种子可以用于复现,但环境变化、插件配置变化可能改变最终顺序。建议在失败报告中记录种子、pytest 配置和依赖安装信息。
第四,fixture 隔离要覆盖所有可变状态,而不仅是全局字典。缓存、单例对象、环境变量、当前工作目录、数据库连接、临时文件、外部服务中的测试数据,都可能成为顺序依赖来源。
第五,模块导入阶段执行的初始化可能绕过 fixture。如果模块级代码创建了连接或缓存,应把初始化移入 fixture,或在 fixture 中显式重置。
第六,涉及数据库或外部服务时,fixture 需要配合事务回滚或清理逻辑,注意事务边界和测试隔离级别,避免测试数据互相污染。
最后,随机化是发现问题的工具,不是测试稳定性的终点。更可靠的目标是:任意顺序、重复运行、并行运行都能得到一致结果。可以把随机顺序运行保留在 CI 中,或定期执行一次,作为防止顺序依赖回归的检查。
- 点赞
- 收藏
- 关注作者
评论(0)