pytest 的核心难点在于测试加载机制、fixture 作用域与 conftest.py 协作逻辑:conftest.py 仅对同级及子目录测试生效且不可 import;fixture 名须严格匹配参数名;parametrize 需注意元组格式、ids 长度及 indirect 使用;pytest.main() 应避免在 fixture 中调用;scope 受 autouse、params 和依赖 fixture 生命周期制约。

pytest 是当前 Python 测试生态的事实标准,但直接上手写 @pytest.mark.parametrize 或调用 pytest.main() 很容易掉进“能跑但不可维护”的坑里。真正卡住人的,从来不是语法,而是它怎么加载测试、怎么管理作用域、怎么和 conftest.py 协作。
为什么你的 conftest.py 总是不生效?
根本原因通常是路径或作用域理解偏差:conftest.py 只对**同级及子目录下的测试文件生效**,且不能被直接 import(否则会报 ImportError: attempted relative import with no known parent package)。
- 确保
conftest.py和你的test_*.py在同一目录,或位于其父目录(但不能跨包跳过__init__.py) - fixture 名称必须和函数参数名**完全一致**,大小写敏感,比如定义了
def db_session():,测试函数就必须写def test_user_create(db_session): - 避免在
conftest.py里写if __name__ == "__main__":—— pytest 加载时会执行整个模块,这类逻辑可能意外触发副作用
pytest.mark.parametrize 的三个典型误用场景
它不是万能的“批量跑”,参数结构不对就会静默跳过或报 TypeError: 'NoneType' object is not iterable。
- 传入单个值时,必须显式写成元组加逗号:
@pytest.mark.parametrize("x", (1,)),写成(1)就是 int,不是 tuple - 多参数组合时,
ids列表长度必须和参数组合数一致,否则报ValueError: ids has wrong length;建议用ids=lambda x: f"input_{x}"动态生成 - 如果参数里含字典或嵌套对象,记得用
indirect=True配合 fixture,否则 pytest 会尝试把 dict 当 fixture 名去查找
如何安全地在测试中调用 pytest.main()?
这不是推荐做法,但 CI 脚本或封装测试入口时偶尔需要。直接调用会导致二次初始化、日志冲突、甚至进程卡死。
- 务必传入
args参数控制行为,例如:pytest.main(["-x", "tests/unit/", "--tb=short"])
- 永远不要在
conftest.py或 fixture 中调用它——作用域混乱会导致 session 级 fixture 重复执行 - 想捕获输出?用
capture=True+plugins参数注入自定义 reporter,而不是重定向sys.stdout
fixture 的 scope 不只是 “function/session”
很多人以为 scope="module" 就是“每个文件一次”,其实它还受 autouse 和 params 影响:
立即学习“Python免费学习笔记(深入)”;
- 带
params的 module 级 fixture,每个参数组合都会触发一次 setup/teardown,不是整个 module 共享一次 -
autouse=True的 fixture 如果设为scope="package",而你又在多个 package 下运行测试,它会在每个 package 初始化时执行,不是全局唯一 - 最隐蔽的坑:
scope="session"的 fixture,如果内部依赖了tmp_path(它是 function 级),会直接报ScopeMismatchError—— 因为生命周期不兼容
assert,而在于搞清哪个 fixture 在哪个时刻被实例化、在哪一层目录下可见、被哪些测试实际消费。这些细节不画图、不打断点跟一次 _pytest.python.PyCollector.collect(),光看文档很难建立直觉。










