pytest 是 Python 里用得最多的测试框架,几乎每个库的 CI 里都有它。它的价值在于断言直接写 assert、失败时能看到两边的具体值、fixture 能把准备和清理拆开。

这篇不聊怎么写测试,只算一笔账:跑几百个用例时,时间到底花在哪。因为测试一旦写多了,每次改一行就跑一遍,等的那几秒会直接影响节奏。

三百个用例的时间去向

样本是 50 个测试文件,每个 6 个用例,共 300 个,全部通过,每个用例只做一次简单断言。跑 3 次取中位,每次前清空 .pytest_cache:

命令墙钟耗时它自己报的时间
pytest --collect-only2700 毫秒收集 300 个用例 0.36 秒
pytest -q4070 毫秒300 passed in 1.72 秒
只跑 1 个用例2234 毫秒1 passed

第三行最能说明问题。只跑一个用例要 2234 毫秒,300 个用例总共 4070 毫秒,也就是说 300 个用例的执行只多花了 1836 毫秒,每个平均 6.14 毫秒。剩下那 2.2 秒是启动加导入加收集的固定成本。

固定开销从哪来

把链条拆开逐个量,每一段都能对上:

python -c pass        753 ms    # 解释器本身
pytest --version     1353 ms    # 加上 pytest 的导入
pytest --collect-only 2700 ms    # 再加上扫描文件和构建用例树
pytest -q            4070 ms    # 最后才是执行

解释器 753 毫秒是 Windows 上的底噪,换 Linux 会低不少。pytest 自己的导入再加 600 毫秒,因为要把断言重写、fixture 管理、插件系统全部装进来。收集阶段又加 1347 毫秒,这一段的成本跟文件数量正相关,跟用例里写了什么无关。

和 unittest 比

同一批 300 个用例改成 TestCase 写法(50 个类,每类 6 个方法),两边各跑 3 次:

运行方式耗时备注
python -m unittest1118 毫秒标准库,没有额外导入
pytest 跑同一批2934 毫秒多 1816 毫秒

差距基本就是 pytest 的导入和收集成本。反过来说,如果项目只有几十个用例、跑一次只花零点几秒,那这 1.8 秒就是纯开销。

一个容易踩的坑:unittest 跑不了裸函数

直接用 python -m unittest discover -s tests 去跑那批 pytest 风格的函数式用例,输出是这一行:

NO TESTS RAN

它不报错,退出码是 0,只是什么都没跑。原因是 unittest 的发现机制只认 TestCase 子类里的方法,顶层 def test_xxx() 完全不认。

这条在迁移时特别容易出问题:把测试从 unittest 改成 pytest 很简单,反过来改回去,用例会静默消失。CI 里如果只判断退出码,等于这批测试从来没跑过。

还有一种不报错的情况:用例函数名不以 test_ 开头时,pytest 不会报错,直接输出 no tests ran in 0.01s,退出码 5。这个 5 在 pytest 里的意思是「一个用例都没收集到」,不是测试失败,和断言挂掉的退出码 1 不是一回事。

什么时候 pytest 不划算

用例总数很少。十来个用例的项目,固定开销 2.2 秒,实际执行不到 0.1 秒,比例太悬殊。这种项目用 unittest 或者干脆写成脚本跑 assert 更直接。

需要把测试打进极小镜像。pytest 依赖十几个包,容器层会明显变厚。标准库的 unittest 是零依赖。

只想快速验证一个函数。改一行跑一遍的场景,用 python -c 直接调比走测试框架快得多。

反过来,用例超过一两百个、需要 fixture 管数据库连接、需要参数化跑几十组数据时,pytest 省下的组织成本远超那 2 秒。

没测的部分列一下,3 项:pytest-xdist 并行跑的实际加速比 / 大型 conftest.py 对收集时间的影响 / 覆盖率插件 coverage 的额外开销。