关于 test-harness
test-harness 是一项从 Python 源码生成可运行 pytest 测试套件的技能,面向编写和维护测试的开发者。它按常规、边界、错误及异步场景规划用例,并结合项目惯例设计 fixture 和 mock。
主要功能
- 01分析函数与依赖关系
- 02覆盖常规、边界和错误场景
- 03规划 fixture 与 mock
- 04提供异步测试指导
- 05支持快速、标准和全面模式
适用场景
- 01为新模块创建 pytest 测试
- 02扩展覆盖范围并避免重复测试
- 03测试含外部依赖或异步代码的模块
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ mathews-tom/ armory --skill test-harness安装后在 Claude Code 中输入/test-harness确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
evals/cases.yamlevals/fixtures/sample_module.pyreferences/async-testing.mdcoverage-targets.mdfixture-design.mdmock-strategies.mdpytest-patterns.md详细介绍
测试框架
系统化生成测试套件,将源代码转换为全面且可运行的 pytest 文件。分析函数签名、依赖关系图和复杂度热点,生成覆盖正常路径、边界条件、错误状态和异步流程的测试,并妥善限定 fixture 的作用域、使用针对性 mock。
参考文件
| 文件 | 内容 | 加载时机 |
|---|---|---|
references/pytest-patterns.md | Fixture 作用域、parametrize、标记、conftest 布局、内置 fixture | 始终 |
references/mock-strategies.md | Mock 决策树、打补丁边界、断言、反模式 | 目标存在外部依赖 |
references/async-testing.md | pytest-asyncio 模式、事件循环 fixture、异步 mock | 目标包含异步代码 |
references/fixture-design.md | 工厂 fixture、yield 清理、作用域选择、组合 | 测试需要复杂设置 |
references/coverage-targets.md | 覆盖率阈值表、分支与行覆盖率、pytest-cov 配置、排除模式 | 请求评估覆盖率时 |
前提条件
- pytest >= 7.0
- Python >= 3.10
- pytest-asyncio — 仅在生成异步测试时需要
- pytest-mock — 可选,可提供
mockerfixture 以替代unittest.mock
工作流
阶段 1:侦察
在编写任何测试之前,先建立目标代码的模型:
- 确定范围 — 哪些函数、类或模块需要测试?如果未指定,请检查近期修改:
git diff --name-only HEAD~5 - 阅读函数签名 — 参数、类型、返回类型和默认值。每个参数都是一个测试维度。
- 梳理依赖 — 哪些调用会访问外部系统(数据库、API、文件系统、时钟)?这些都是 mock 候选项。
- 检测复杂度热点 — 分支数量多、嵌套较深或有多条返回路径的函数需要更多测试用例。
- 检查现有测试 — 如果已有测试,了解其覆盖范围。不要重复编写;应在此基础上扩展。
- 阅读项目规范 — 检查 CLAUDE.md、conftest.py、pytest.ini/pyproject.toml,了解项目已采用的 fixture、标记和测试组织模式。
阶段 2:枚举测试用例
对每个待测函数,按以下四个类别枚举用例:
| 类别 | 测试内容 | 示例 |
|---|---|---|
| 正常路径 | 预期输入产生预期输出 | add(2, 3) 返回 5 |
| 边界 | 有效输入范围边缘的值 | 空字符串、零、最大整数、单个元素 |
| 错误 | 无效输入触发适当的异常 | 需要 str 时传入 None、负索引 |
| 状态 | 状态转换产生正确的副作用 | 对象从 pending 转为 active |
对于每个用例,记录:
- 输入值(具体值,而非抽象描述)
- 预期输出或异常
- 所需的设置(fixture)
- 所需的 mock(用于抑制外部调用)
对于测试逻辑相同、仅输入或输出值不同的用例,使用 parametrize。
第 3 阶段:夹具设计
-
识别共享设置 — 如果 3 个或更多测试需要同一个对象,就提取一个夹具。
-
选择作用域 — 使用能避免重复设置的最小作用域:
作用域 使用场景 示例 function默认。每个测试都会获得全新状态 大多数单元测试 class同一个类中的测试共享开销较大的设置 每个测试类共用一个 DB 连接 module同一个文件中的所有测试共享设置 已加载的配置文件 session整个测试运行过程共享设置 Docker 容器启动 -
设计清理流程 — 需要清理时,使用
yield夹具。测试结束后,绝不遗留 副作用(临时文件、DB 行、monkey-patch)。 -
识别 conftest 候选项 — 多个测试文件都会用到的夹具应放在
conftest.py中。仅在一个文件中使用的夹具则留在该文件中。
第 4 阶段:模拟策略
-
确定要模拟的内容 — 仅模拟外部依赖:
- 网络调用(API、数据库、消息队列)
- 文件系统操作(测试逻辑而非 I/O 时)
- 依赖时间的行为(
datetime.now、time.sleep) - 随机或非确定性行为
-
确定不应模拟的内容 — 绝不模拟:
- 被测函数
- 被测目标调用的纯函数(通过被测目标对其进行测试)
- 数据结构和值对象
-
选择模拟层级 — 在被测模块的导入边界处打补丁, 而不是在定义处。使用
@patch('mymodule.requests.get'),不要使用@patch('requests.get')。 -
添加模拟断言 — 每个模拟都应断言它使用预期参数、按预期次数调用。没有断言的模拟会造成覆盖率盲区。
第 5 阶段:输出
按照以下结构生成测试文件:
- 导入项(pytest、模拟对象、目标模块)
- 常量和测试数据
- 夹具(按作用域排序:session > module > class > function)
- 按目标函数分组的测试类或函数
- 适用时使用参数化测试
输出格式
# tests/test_{module}.py
import pytest
from unittest.mock import Mock, patch, MagicMock
from {module} import {target_function, TargetClass}
# ============================================================
# Fixtures
# ============================================================
@pytest.fixture
def valid_input():
"""Standard valid input for happy path tests."""
return {concrete values}
@pytest.fixture
def mock_database():
"""Mock database connection."""
with patch("{module}.db_connection") as mock_db:
mock_db.query.return_value = [{expected data}]
yield mock_db
# ============================================================
# {target_function} Tests
# ============================================================
class TestTargetFunction:
"""Tests for {target_function}."""
def test_happy_path(self, valid_input):
"""Returns expected result for valid input."""
result = target_function(valid_input)
assert result == {expected}
@pytest.mark.parametrize(
"input_val, expected",
[
({boundary_1}, {expected_1}),
({boundary_2}, {expected_2}),
({boundary_3}, {expected_3}),
],
ids=["empty", "single", "maximum"],
)
def test_boundary_conditions(self, input_val, expected):
"""Handles boundary inputs correctly."""
assert target_function(input_val) == expected
def test_invalid_input_raises(self):
"""Raises TypeError for invalid input."""
with pytest.raises(TypeError, match="expected str"):
target_function(None)
def test_external_call(self, mock_database):
"""Calls database with correct query."""
target_function("lookup_key")
mock_database.query.assert_called_once_with("SELECT * FROM t WHERE key = %s", ("lookup_key",))
配置作用域
| 模式 | 作用域 | 深度 | 使用场景 |
|---|---|---|---|
quick | 单个函数 | 正常路径 + 1 个错误用例 | 快速迭代、TDD 红绿循环 |
standard | 文件或类 | 正常 + 边界 + 错误 + 模拟 | 大多数请求的默认选项 |
comprehensive | 模块或包 | 所有类别 + 异步 + 参数化矩阵 | 发布前、关键路径代码 |
校准规则
- 测试隔离不可妥协。 每个测试都必须单独运行时通过,也必须在任意顺序下通过。 任何测试都不得依赖其他测试产生的副作用。
- 严格管理 Mock。 Mock 外部依赖,而非内部逻辑。过度 Mock 会导致代码出错时测试仍然通过;Mock 不足则会导致网络中断时测试失败。
- 具体优于抽象。 测试数据必须使用具体值,而非占位符。使用
"[email protected]",而不是"test_email";使用42,而不是"some_number"。具体值可以捕获抽象占位符掩盖的类型不匹配。 - 每个测试聚焦一项断言。 一个测试应验证一种行为。如果多个断言验证的是同一种行为的不同方面(例如返回值和副作用),则可以接受;但不要用它们验证无关行为。
- 使用参数化,而非重复测试。 如果两个测试的区别仅在于输入/输出值,请使用
@pytest.mark.parametrize将它们合并。使用ids设置易读的测试名称。 - 遵循项目约定。 如果项目使用
conftest.pyfixture、基于类的测试或特定标记,请遵循这些模式。不要引入与之冲突的测试风格。
错误处理
| 问题 | 解决方法 |
|---|---|
| 目标函数没有类型注解 | 根据用法模式、默认值和文档字符串推断类型。在测试文档字符串中注明不确定之处。 |
| 目标函数有深层嵌套的依赖项 | 在最靠近被测函数的边界处进行 Mock。不要分别 Mock 传递依赖项。 |
| 没有现成的测试基础设施(没有 conftest,也没有 pytest 配置) | 在测试文件旁生成一个最小化的 conftest.py。在输出中注明这一新增文件。 |
| 目标代码不可测试(存在全局状态或隐藏依赖项) | 在输出中指出设计问题。为可测试的部分生成测试,并建议重构以提升可测试性。 |
| 检测到异步代码,但未安装 pytest-asyncio | 注明依赖要求。使用 @pytest.mark.asyncio 生成异步测试样板,并告知用户进行安装。 |
| 无法导入目标模块 | 报告导入错误。不要为无法导入的代码生成测试。 |
不应生成测试的情况
遇到以下情况时,请提出异议:
- 代码是自动生成的(protobuf、OpenAPI 客户端、ORM 模型)——应测试生成器或模式,而非生成结果
- 请求针对 UI/E2E 测试——此技能仅生成单元测试和集成测试
- 代码没有明确可测试的行为(纯配置、常量定义)
- 用户要求为第三方库代码编写测试——应测试你对该库的使用方式,而非库本身
合理化借口
| 合理化借口 | 事实 |
|---|---|
| “手动测试就足够了” | 手动测试不会在 CI 中运行,无法发现回归问题,也无法随代码库增长而扩展 |
| “这段代码太简单,不需要测试” | 简单的代码会变复杂——测试可以记录预期行为,并发现未来改动引起的回归问题 |
| “我以后再添加测试” | 测试是规范;没有测试,代码行为就没有定义,而所谓的“以后”永远不会到来 |
| “全部使用模拟就能让测试更快” | 过度使用模拟的测试可能在真实系统失败时依然通过——应在边界处模拟,而不是在调用链深处模拟 |
| “100% 的覆盖率意味着代码正确” | 覆盖率衡量的是代码执行情况,而非正确性——运行代码却没有有意义断言的测试毫无价值 |
| “只测试正常路径就够了” | 边界情况和错误路径是大多数生产事故的原因——只测试正常路径会让人产生虚假的信心 |
危险信号
- 测试只覆盖正常路径,不包含边界情况或错误路径
- 测试名称描述实现(“test_calls_function”),而非行为(“test_returns_404_when_not_found”)
- 单个测试中使用了两个以上的模拟对象——这表明被测单元的耦合度过高
- 测试依赖执行顺序或共享的可变状态
- 断言实现细节(模拟调用次数),而非可观察到的行为
- 以“单元测试已经覆盖”为由跳过集成测试
验证
- 测试遵循 Arrange-Act-Assert 结构,各阶段划分清晰
- 测试名称描述行为:
test_<unit>_<scenario>_<expected_outcome> - 覆盖边界情况:空输入、边界值、错误路径、null/None
- 覆盖率达到阈值:总体 80%,新增代码 90%,关键路径 95%
- 所有测试均通过:
pytest/npm test以退出码 0 结束,并捕获输出 - 没有测试依赖执行顺序——可以按任意顺序运行
- 仅在边界处使用模拟(外部 API、系统时钟、单元测试中的文件系统)