pytest通过自定义插件钩子函数实现模块级测试统计:在pytest_runtest_makereport中用item.module.__name__提取模块名并计数,在pytest_sessionfinish中汇总输出通过率。

pytest中如何获取每个模块的测试结果统计
pytest本身不直接提供按模块(即 Python 文件)粒度的通过率统计,必须通过钩子函数在测试生命周期中收集数据。核心是监听 pytest_runtest_makereport 和 pytest_sessionfinish,前者捕获每个测试项的状态,后者汇总输出。
关键点在于:测试项的模块信息需从 item.module.__name__ 提取,而不是 item.fspath —— 后者是路径,不同导入方式可能导致同一模块被识别为多个路径(比如通过 -p 参数或 conftest.py 跨目录导入时)。
-
item.module.__name__是唯一可靠的模块标识(如"test_utils"、"tests.test_api") - 避免用
item.fspath.purebasename,它只返回文件名(如"test_api"),无法区分tests/api/test_auth.py和tests/core/test_auth.py) - 所有状态判断应基于
report.outcome in ("passed", "failed", "skipped"),不要依赖report.when == "call"之外的阶段(如"setup"或"teardown"可能重复触发)
自定义插件的最小可运行结构
插件只需一个 Python 文件(如 pytest_module_rate.py),放在项目根目录或 conftest.py 同级,并通过 pytest_plugins = ["pytest_module_rate"] 显式启用,或直接用 -p pytest_module_rate 加载。
以下是最简可行逻辑:
立即学习“Python免费学习笔记(深入)”;
让 macOS 自动化像说话一样简单 —— 用代码控制你的 Mac,无需学习 AppleScript 系统要求 macOS 14+ (Sonoma) Python 3.9+ 需要开启:辅助功能 + 屏幕录制权限
import pytest
from collections import defaultdict
<p>stats = defaultdict(lambda: {"total": 0, "passed": 0})</p><p>def pytest_runtest_makereport(item, call, report):
if report.when != "call":
return
mod_name = item.module.<strong>name</strong>
stats[mod_name]["total"] += 1
if report.outcome == "passed":
stats[mod_name]["passed"] += 1</p><p>def pytest_sessionfinish(session, exitstatus):
print("\n=== Module pass rates ===")
for mod, s in sorted(stats.items()):
rate = s["passed"] / s["total"] * 100 if s["total"] else 0
print(f"{mod:<25} {s['passed']}/{s['total']} ({rate:.1f}%)")
注意:pytest_runtest_makereport 的参数签名必须严格匹配(item, call, report),旧版 pytest(
为什么不能只靠 --tb=short 或 --verbose 实现
这些命令行选项只控制输出格式,不改变数据收集行为。即使开启 --verbose,pytest 仍按测试函数(test_*)逐条打印,不会自动聚合成模块维度。人工统计 100+ 个测试时极易漏数或误判——尤其当某模块含 parametrize 生成多个用例时,单个 test_foo 可能对应 5 个 test_foo[case1] 等子项,但它们同属一个模块。
-
parametrize生成的每个实例都触发一次pytest_runtest_makereport,所以上述插件天然支持,无需额外处理 - 若模块内有
pytest.skip()或@pytest.mark.skip,其report.outcome为"skipped",会被计入total但不加passed,符合“通过率 = 通过数 / 总执行数”的业务定义 - 不建议把
skipped单独剔除——跳过的测试本就是该模块有效测试集的一部分
集成到 CI 时要注意的兼容性问题
CI 环境(如 GitHub Actions、GitLab CI)通常使用 pytest --junitxml=report.xml 生成报告,而自定义插件的 print 输出默认写入 stdout,可能被日志截断或淹没。更可靠的做法是将结果写入文件:
- 在
pytest_sessionfinish中改用with open("module_rate.txt", "w") as f:写入结构化文本 - 避免使用
sys.stdout.write或logging,因为 pytest 会接管日志流,导致输出错乱 - 如果同时启用
--tb=no或--quiet,插件的print仍会生效(因它在 session 结束时执行,不受 verbosity 控制) - 多进程执行(
-n auto)下,此插件不可用——各 worker 进程无法共享stats字典,必须改用pytest-xdist兼容方案(如通过worker_id+ 文件锁 + JSON 合并)
模块级通过率真正有用的地方,是快速定位“哪个文件开始腐化”,而不是追求精确到小数点后两位。实际落地时,把阈值硬编码进插件(比如低于 80% 就 sys.exit(1))反而容易引发误伤——有些模块天生就该低通过率(如集成测试依赖外部服务)。先看清数据,再决定怎么用。

















