关于 python-testing
该技能为 Python 包建立测试基础设施,面向需要可靠测试门槛和多版本验证的项目。它涵盖 pytest 配置、分支覆盖率、Hypothesis、变异测试及多版本运行。
主要功能
- 01检查并统一 pytest 配置源
- 02启用分支覆盖率门槛
- 03验证覆盖率门槛确实失败
- 04使用 Hypothesis 属性测试
- 05通过变异测试检查断言
- 06运行多 Python 版本测试
适用场景
- 01为 Python 包配置 pytest
- 02防止覆盖率门槛静默失效
- 03检查测试是否能发现代码错误
- 04验证多个 Python 版本兼容性
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ paldom/ python-skills --skill python-testing安装后在 Claude Code 中输入/python-testing确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
evals/evals.jsonreferences/beyond-coverage.mdconfiguration.mdmulti-version.mdscripts/check_test_config.py详细介绍
python-testing
为 Python 包构建可靠的测试基础设施,使其在压力下依然值得信赖:确保已安装的 pytest 实际读取 pytest 配置;设置分支覆盖率门槛并验证它确实会导致失败;通过 Hypothesis 和变异测试,防止测试只执行代码却不检查结果;并运行多个版本。此技能要防止的问题是测试结果虽为绿色、实际却毫无意义——门槛悄然失效、配置表被悄然忽略,以及测试套件被人类或 Agent 修改到无法再证明任何东西。
不适用场景
- 为单个文件或函数编写测试,或改进某个模块的测试——直接处理即可,无需基础设施。
- 诊断或修复某个失败的测试——阅读测试和被测代码。
- CI 工作流 YAML——GitHub Actions 作业、版本矩阵配置、跨 CI 执行分支上传/合并覆盖率。若已安装 python-ci 技能,应由它负责此项。本技能决定运行什么;CI 决定在哪里运行。
- 打包工作——
pyproject元数据、构建后端,或作为打包工作将仓库转换为src布局。若已安装 python-packaging 技能,应由它负责此项。 - Git 钩子配置——由 python-precommit 技能负责;完整测试套件本来也不该放在 pre-commit 钩子里。
工作流
1. 检查仓库
先运行只读审计——它能发现被遮蔽和分散的配置来源:
python3 "${CLAUDE_SKILL_DIR}/scripts/check_test_config.py" --root .
然后确认后续步骤所需的信息:
uv run pytest --version # major version decides the config table (step 2)
grep -n "requires-python" pyproject.toml
ls pytest.ini setup.cfg tox.ini .coveragerc noxfile.py 2>/dev/null
注意项目布局:src/<pkg>/ 加同级 tests/(src 布局),或包位于仓库根目录(扁平布局)。全文统一约定:通过 uv 调用工具(uv run、uvx)。如果没有 uv,普通替代方案是先运行 python -m pip install -e ".[dev]",再运行 python -m pytest——各处使用相同的标志;这里只说明一次。
2. pytest 配置——在 pyproject.toml 中使用一张配置表
存在两种配置表,具体使用哪一种取决于已安装的主版本:pytest >= 9.0(2025 年 11 月)会读取原生 [tool.pytest] 配置表(使用真正的 TOML 数组/布尔值);所有受支持的 pytest 版本都会读取 [tool.pytest.ini_options](INI 风格的字符串)。切勿同时编写两种配置表。在 pytest 低于 9 时,[tool.pytest] 会被静默忽略:不会报错,测试仍会运行,但严格性标志不会生效——因此先检查 uv run pytest --version(https://docs.pytest.org/en/stable/reference/customize.html)。下面的配置块使用 ini_options,因为它适用于所有版本;对于 >= 9 的仓库,优先使用包含相同键的原生配置表。
[tool.pytest.ini_options]
minversion = "8.0" # raise to "9.1" if you rely on the strict flags below via addopts (see gotchas)
testpaths = ["tests"]
addopts = "-ra --strict-markers --strict-config"
filterwarnings = ["error"]
--strict-markers——拼写错误或未声明的@pytest.mark.*标记会导致失败,而不是被静默忽略。--strict-config——拼写错误的配置键会变成错误。filterwarnings = ["error"]——弃用警告现在就会导致失败,而不是等到升级时才发现。在旧版依赖树中,这可能产生大量警告;应添加针对性的"ignore:<msg>:<category>"条目,而不是删除这道门槛。- 异步项目?添加
asyncio_mode = "auto"(并将 pytest-asyncio 作为开发依赖),即可省去每个测试上的标记。 - 不要将
--cov放入addopts。让每次运行都依赖覆盖率会拖慢单个测试的迭代循环,还可能干扰调试器断点;请在下方明确的命令和 CI 中传入覆盖率标志。权衡之处:有些团队会将它保留在addopts中,以免忘记覆盖率——如果用户更倾向于这样做,就保留它,并说明单次运行可用--no-cov禁用。 - 只保留一个配置来源。 pytest 会使用以下配置中的第一个:
pytest.toml/.pytest.toml、pytest.ini/.pytest.ini(即使为空也优先)、pyproject.toml(含任一配置表)、tox.ini、setup.cfg,并忽略其余配置——多余的pytest.ini会悄然遮蔽pyproject中的所有配置。合并配置后,再删除多余文件。
3. 布局与发现
src 布局(src/<pkg>/,测试放在同级的 tests/ 中,而非包内)是可靠的结构:使用 uv run 时,pytest 导入的是已安装的包,因此导入和打包错误会在测试中暴露,而不是等到用户的环境中才出现(https://docs.pytest.org/en/stable/explanation/goodpractices.html)。
此时,testpaths = ["tests"] 和 coverage 的 source = ["src"] 也能彼此对应。
- 刚迁移到
src布局后,如果出现“pytest 找不到/无法导入我的包”,通常意味着该包尚未安装到环境中——运行uv sync(或pip install -e .),然后重新运行。 - 不要仅为了测试就把扁平布局的仓库迁移到
src布局;这属于打包变更(应由 python-packaging 处理)。扁平布局也能正常测试——只是无法保证测试的是已安装的包。 - 共享 fixture 放在
tests/conftest.py中。
4. 覆盖率——启用分支覆盖,验证门槛
[tool.coverage.run]
source = ["src"]
branch = true
parallel = true
relative_files = true
[tool.coverage.report]
fail_under = 85
show_missing = true
skip_covered = true
exclude_lines = [
"pragma: no cover",
"if TYPE_CHECKING:",
"raise NotImplementedError",
"if __name__ == .__main__.:",
]
- 关键在于
branch = true——行覆盖率表示某行运行过;分支覆盖率表示每个if的两侧都执行过。仅看行覆盖率会高估覆盖情况。 fail_under设为 80–90 是合理区间。不建议设为 100%:这会催生没有断言的测试和大量pragma。应排除确实无法到达的代码行。- 如果存在
.coveragerc,它会悄然覆盖 pyproject 中的[tool.coverage.*]配置——只保留一个配置源。 - 使用
uv run pytest --cov --cov-report=term-missing运行。
验证门槛。 pytest-cov 7.1 之前,在某些报告选项下,未达到总体阈值时可能仍返回退出代码 0(已于 7.1.0 修复,2026-03);而且曾出现过 66.6% 因四舍五入而通过 67 的门槛。切勿想当然——要实际验证:
uv run pytest --cov --cov-fail-under=100 -q; echo "exit=$?"
预期会返回非零退出代码(除非覆盖率确实为 100%)。如果返回 0,就分两步设置门槛:先运行 uv run pytest --cov,再运行 uv run coverage report --fail-under=85——独立 coverage 命令的退出代码是可靠的。
- 并行运行(通过 pytest-xdist 使用
pytest -n auto)需要设置parallel = true和relative_files = true。测试启动的子进程需要配置[tool.coverage.run] patch = ["subprocess"](coverage >= 7.10)——pytest-cov 7 移除了自身的子进程测量功能——否则覆盖率结果会为空或为 0%。 - 阈值只是下限,不是质量指标——没有断言的测试也能轻易达标。第 5–6 步是应对措施。PR 的补丁/增量覆盖率需要在 CI 中配置(如果已安装,可使用 python-ci skill)。
各项配置的详细说明、优先级规则和合并机制: references/configuration.md。
5. 在适用场景中使用基于属性的测试
对于解析器、序列化器、编解码器和算法代码,只要存在可验证的属性(往返、幂等性、不变量),就添加 Hypothesis。一项基于 426 个项目的语料库研究发现,每个基于属性的测试能杀死的变异数约为平均单元测试的 50 倍(https://dl.acm.org/doi/10.1145/3764068)。
uv add --dev hypothesis # exact version pinned by uv.lock
from hypothesis import given, strategies as st
@given(st.text())
def test_roundtrip(s):
assert decode(encode(s)) == s
- 优先使用有界策略(
st.integers(min_value=1)),而不是通过assume()过滤——拒绝大多数样例会触发HealthCheck.filter_too_much。 - 在
tests/conftest.py中注册设置配置文件;CI 通过pytest --hypothesis-profile=ci运行较重的配置:
from hypothesis import settings
settings.register_profile("ci", max_examples=1000)
settings.register_profile("dev", max_examples=25)
- 持久化
.hypothesis/示例数据库(提交它,或在 CI 中缓存)——否则每次运行都会从头重新发现已知反例。 DeadlineExceeded只在 CI 机器上出现,意味着共享运行器速度较慢——应在 ci 配置文件中有意调高或禁用deadline;不要删除测试。
属性测试模式、策略设计和有状态测试: references/beyond-coverage.md。
6. 变异测试——测试能发现 bug 吗?
覆盖率只能证明某行代码执行过;变异测试则能证明该行代码出错时,测试会失败。这是直接检验断言质量的方法;当 Agent 编写或修改测试时,这一点尤为重要:把测试改到通过的套件本身并不能证明任何事。可选工具——尚无公认的赢家,请根据约束选择:
| 工具 | 特点 | 注意事项 |
|---|---|---|
| mutmut | 成熟,CLI 最简单,以 pytest 为先 | 全仓库运行很慢——限定 paths_to_mutate |
| cosmic-ray | 基于会话,可恢复、可分布式运行 | 设置最繁琐;对小型包来说大材小用 |
| pytest-gremlins | 以 pytest 插件形式运行 | 较新;采用情况的证据不足——先评估维护状况 |
将其安装为开发依赖,使其在项目环境中运行,并由 uv.lock 锁定(未锁定的 uvx 临时运行会执行最近发布的任意版本——这会造成可复现性和供应链风险):
uv add --dev mutmut
uv run mutmut run
- 首次运行时,将范围限定到一个核心模块(
[tool.mutmut]中的source_paths;对生成代码使用do_not_mutate), 然后检查未被发现的变异:存活的变异意味着缺少或较弱的断言—— 也可能是确实等价的变异,这时标记并继续即可。 - 第一天不要把变异分数设为 CI 门槛;限定范围运行或安排夜间运行,并将 分数视为诊断指标——mutmut 3.7.0 自身尚未发布的说明中仍列有变异覆盖率排除行、假阳性存活项,以及将超时计为杀死的情况。实践者报告称, 85–95% 的变异分数比相同数值的行覆盖率有意义得多。
变异测试的检查流程及 Agent 时代的采用理由: references/beyond-coverage.md。
7. 多版本运行
根据项目选择工具——这场讨论确实尚无定论:
| 情况 | 选择 |
|---|---|
| 依赖简单,特定版本分支较少 | 使用普通的 uv 循环(见下文) |
| 复杂的矩阵(额外依赖、后端、C 扩展) | 使用 tox 或 nox,并以 uv 加速 |
| 需要 Python(而非 INI)配置和动态会话 | 使用 nox |
| 已在使用 tox | 保留现有配置;添加 tox-uv——加快运行,无需重写配置 |
for v in 3.11 3.12 3.13 3.14; do # the versions requires-python promises; 3.10 is EOL 2026-10
uv run -p "$v" --with pytest --with pytest-cov pytest || exit 1
done
对于 tox 或 nox,将环境后端切换为 uv(tox-uv、nox 的 uv 后端):
无需更改配置模型,即可大幅加快环境设置速度。
- nox 的一个陷阱:仓库根目录中的
.python-version文件可能会悄悄将每个 会话都固定到本地开发版本(https://github.com/wntrblm/nox/issues/1038)。 - 切勿在每个版本分支中强制执行
fail_under——仅存在于 3.13 的分支会降低 3.10 分支上的覆盖率。本地运行时:使用parallel = true运行循环,然后 只运行一次uv run coverage combine && uv run coverage report --fail-under=85。CI 中实现这种拆分的方式(制品、合并任务)属于 python-ci。
配置与权衡详解: references/multi-version.md。
8. 验证
uv run pytest——全部通过。- 第 4 步中展示的门槛触发示例以非零状态码退出。
python3 "${CLAUDE_SKILL_DIR}/scripts/check_test_config.py" --root .以状态码 0 退出。- 向用户报告:配置位置和选择的表格(及原因)、阈值,以及已接入或延后的防护措施(Hypothesis、变异测试、多版本运行)。
输出规范
满足以下条件即为完成:
- 恰好只有一处 pytest 配置源和一处覆盖率配置源;pytest 表格与已安装的主版本匹配。
- 已启用分支覆盖率;
fail_under处于 80–90 区间;已演示遗漏时门槛会以非零状态码退出。 - 在干净检出的代码上,使用
uv run pytest可使测试套件全部通过。 - 如有要求:已添加 Hypothesis 开发依赖并配置 profiles;已固定变异测试工具版本并完成首次限定范围的运行与检查;已选择多版本运行工具并说明理由。
- 未编写或修改任何 CI YAML。
故障模式与注意事项
| 症状 | 原因 / 修复方法 |
|---|---|
| 覆盖率门槛在低于阈值时仍“通过” | pytest-cov < 7.1 的汇总退出码缺陷或舍入问题;验证门槛(步骤 4),或通过独立的 coverage report --fail-under 设置门槛 |
| 严格性标志莫名未生效 | pytest <9 时使用 [tool.pytest](会被静默忽略)、存在覆盖配置的 pytest.toml/pytest.ini,或使用 pytest 9.0.x——一个回归问题会静默忽略 addopts 中的 --strict-markers/--strict-config(已在 9.1.0 中修复,2026-06);在 >= 9 的版本中,优先使用 strict_markers/strict_config ini 键 |
使用 pytest -n auto 时覆盖率为 0% 或为空 | 缺少 parallel/relative_files;由测试启动的子进程需要 coverage 的 patch = ["subprocess"](pytest-cov 7 不再测量这些子进程) |
| 修改覆盖率配置后毫无变化 | .coveragerc 的优先级高于 pyproject——删除其中一个 |
| 门槛脚本检查特定退出码 | 各工具的行为不同:coverage.py 的 fail_under 退出码为 2,pytest-cov 为 1,未收集到测试时 pytest 为 5——应检查是否非零 |
fail_under = 90 且 branch = true 时,分支覆盖率为 85% 仍“通过” | 总覆盖率会合并语句和分支的覆盖机会,并非仅针对分支的最低值——查看分支列,或单独设置分支门槛 |
| 矩阵任务因特定版本的代码而失败 | fail_under 会在每个任务中执行;应先合并,再检查门槛 |
| Agent 编辑测试后,测试套件立刻全绿 | 这不能证明任何问题——应先审查测试差异,再审查源代码差异,并运行变异测试;研究人员已证明 Agent 会通过注入 conftest.py 伪造全绿结果,因此应在 Agent 无法写入的环境中验证 |
| 生成的测试通过,却未能发现回归 | 过度依赖模拟的测试,这是有文献记录的 Agent 偏差(https://arxiv.org/abs/2602.00409)——应要求针对公共 API 的行为断言,而非模拟调用次数 |
Hypothesis 拒绝了大多数示例(filter_too_much) | 用有界策略替换 assume() |
| Hypothesis 找到了缺陷,但下一次运行时忘记了 | .hypothesis/ 数据库未在运行之间持久化 |
仅在 CI 中出现不稳定的 DeadlineExceeded | 共享运行器速度较慢——调整 ci 配置中的 deadline |
文件
scripts/check_test_config.py— pytest/coverage 配置的只读审计 (被覆盖的来源、错误的配置表、缺少分支覆盖率或门槛)。 发现错误时以非零状态码退出;绝不修改仓库。references/configuration.md— pytest 和 coverage 配置详解: 优先级、pytest 9 的配置表问题、退出码缺陷、xdist/合并机制。references/beyond-coverage.md— Hypothesis 模式与设置;变异 工具比较与存活变异分析;为何 Agent 编辑后全绿的测试套件需要 额外保障。references/multi-version.md— uv 循环与 tox、nox 的比较,基于 uv 的运行器, 跨版本覆盖率。