根本原因是 coverage.py 的 sys.settrace() 在模块顶层执行时被线程初始化中断,导致 import 和常量赋值虽已执行却未被记录;典型诱因是 apiclient() 等初始化触发后台线程,使 trace hook 静默失效。

为什么 import 和常量行总显示为未覆盖
这不是配置漏了,而是 coverage.py 的 sys.settrace() 在模块顶层执行时被意外中断。典型诱因是某行初始化代码(比如 api_client = ApiClient())触发了线程池启动,新线程不继承 trace hook,主线程的追踪上下文也被静默重置——结果就是 import logging、CONST = 42 这些早已执行过的语句,在覆盖率数据里变成 MISSING。
验证方法很简单:在可疑模块第一行加 import pdb; pdb.set_trace(),跑 coverage run -m pytest,如果执行到 ApiClient() 后 n 命令直接跳过后续行,基本可确认是这个机制问题。
- 别指望 mock 掉它来“修复”——mock 只能绕过副作用,但 trace 中断发生在 import 阶段,mock 晚了一步
- 不要把初始化挪到函数里再调用——模块导入时仍会执行,没解决问题
- 真正有效的做法是彻底延迟:把
ApiClient实例化推迟到首次使用时
--cov 参数不生效,源码根本不出现在报告里
最常见原因是 pytest-cov 默认只跟踪当前工作目录下被导入的模块,而你的 src/ 或 myproject/ 目录没被 Python 解释器识别为可导入路径。现象是报告里只有 test_*.py,业务代码完全不出现——这不是覆盖率低,是压根没扫描。
必须显式告诉工具“哪些才是我的源码”:
- 在
pyproject.toml里写死:[tool.coverage.run] source = ["src"](推荐,比命令行参数稳定) - 如果不用
src/结构,改用--cov=myproject,但要确保myproject/__init__.py存在且能被正常 import - 避免用
python -m pytest启动——-m会临时修改sys.path,导致源码路径失效;统一用pytest命令
报告显示 100%,但 if/else 分支明显没跑全
pytest-cov 默认只统计行覆盖(line coverage),对分支逻辑完全无感。所谓“100%”只是每行都执行过,不代表每个 if 的 else、每个 try 的 except 都被触发。
必须启用分支覆盖才能暴露真实缺口:
- 加
--cov-branch参数,或配置[tool.coverage.run] branch = true - HTML 报告里未覆盖分支会标红,并在行尾显示类似
1->2的跳转缺失提示 - 参数化测试尤其容易踩坑:如果
@pytest.mark.parametrize只传正数,if x > 0:的else分支永远沉默——补上0、-1、None等边界值,再跑一次
子进程里的代码没被统计,覆盖率虚高
用 multiprocessing、subprocess 或 Celery 的项目,子进程默认不被 pytest-cov 跟踪。主进程报告 95%,但关键的 worker 逻辑实际裸奔。
要让子进程也上报覆盖率,得靠环境变量注入机制:
- 确保所有子进程启动前设置了
COV_CORE_SOURCE、COV_CORE_CONFIG、COV_CORE_DATAFILE这三个变量 - 如果用
subprocess.Popen,手动传入env={**os.environ, "COV_CORE_SOURCE": "src"} - 如果是
multiprocessing,需在if __name__ == "__main__"块里显式调用coverage.process_startup()(新版coverage.py支持) - CI 环境中常漏掉这点——worker 节点必须装
pytest-cov,否则环境变量无效
真正的难点不在配置,而在验证:跑完测试后检查 .coverage 文件是否包含子进程生成的数据片段,而不是只信终端里那个漂亮的百分比数字。
Python免费学习笔记(深入):立即使用
在学习笔记中,你将探索 Python 的核心概念和高级技巧!











