mcpField
P
python-testing

python-testing

github.com/paldom测试与质量262 次安装7 周前更新
代码仓库

关于 python-testing

该技能为 Python 包建立测试基础设施,面向需要可靠测试门槛和多版本验证的项目。它涵盖 pytest 配置、分支覆盖率、Hypothesis、变异测试及多版本运行。

主要功能

  1. 01检查并统一 pytest 配置源
  2. 02启用分支覆盖率门槛
  3. 03验证覆盖率门槛确实失败
  4. 04使用 Hypothesis 属性测试
  5. 05通过变异测试检查断言
  6. 06运行多 Python 版本测试

适用场景

  1. 01为 Python 包配置 pytest
  2. 02防止覆盖率门槛静默失效
  3. 03检查测试是否能发现代码错误
  4. 04验证多个 Python 版本兼容性

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/paldom/python-skills --skill python-testing

安装后在 Claude Code 中输入/python-testing确认已加载。

包含的文件

除 SKILL.md 外,安装时一起下载的参考资料和脚本。

evals/
evals.json
references/
beyond-coverage.md
configuration.md
multi-version.md
scripts/
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. 验证
  1. uv run pytest——全部通过。
  2. 第 4 步中展示的门槛触发示例以非零状态码退出。
  3. python3 "${CLAUDE_SKILL_DIR}/scripts/check_test_config.py" --root . 以状态码 0 退出。
  4. 向用户报告:配置位置和选择的表格(及原因)、阈值,以及已接入或延后的防护措施(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 的运行器, 跨版本覆盖率。

评论

支持 Markdown