mcpField
T
test-harness

test-harness

github.com/mathews-tom测试与质量339102 次安装2 天前更新
代码仓库

关于 test-harness

test-harness 是一项从 Python 源码生成可运行 pytest 测试套件的技能,面向编写和维护测试的开发者。它按常规、边界、错误及异步场景规划用例,并结合项目惯例设计 fixture 和 mock。

主要功能

  1. 01分析函数与依赖关系
  2. 02覆盖常规、边界和错误场景
  3. 03规划 fixture 与 mock
  4. 04提供异步测试指导
  5. 05支持快速、标准和全面模式

适用场景

  1. 01为新模块创建 pytest 测试
  2. 02扩展覆盖范围并避免重复测试
  3. 03测试含外部依赖或异步代码的模块

安装

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

$npx skills add https://github.com/mathews-tom/armory --skill test-harness

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

包含的文件

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

evals/
cases.yaml
evals/fixtures/
sample_module.py
references/
async-testing.md
coverage-targets.md
fixture-design.md
mock-strategies.md
pytest-patterns.md

详细介绍

测试框架

系统化生成测试套件,将源代码转换为全面且可运行的 pytest 文件。分析函数签名、依赖关系图和复杂度热点,生成覆盖正常路径、边界条件、错误状态和异步流程的测试,并妥善限定 fixture 的作用域、使用针对性 mock。

参考文件

文件内容加载时机
references/pytest-patterns.mdFixture 作用域、parametrize、标记、conftest 布局、内置 fixture始终
references/mock-strategies.mdMock 决策树、打补丁边界、断言、反模式目标存在外部依赖
references/async-testing.mdpytest-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 — 可选,可提供 mocker fixture 以替代 unittest.mock

工作流

阶段 1:侦察

在编写任何测试之前,先建立目标代码的模型:

  1. 确定范围 — 哪些函数、类或模块需要测试?如果未指定,请检查近期修改:git diff --name-only HEAD~5
  2. 阅读函数签名 — 参数、类型、返回类型和默认值。每个参数都是一个测试维度。
  3. 梳理依赖 — 哪些调用会访问外部系统(数据库、API、文件系统、时钟)?这些都是 mock 候选项。
  4. 检测复杂度热点 — 分支数量多、嵌套较深或有多条返回路径的函数需要更多测试用例。
  5. 检查现有测试 — 如果已有测试,了解其覆盖范围。不要重复编写;应在此基础上扩展。
  6. 阅读项目规范 — 检查 CLAUDE.md、conftest.py、pytest.ini/pyproject.toml,了解项目已采用的 fixture、标记和测试组织模式。
阶段 2:枚举测试用例

对每个待测函数,按以下四个类别枚举用例:

类别测试内容示例
正常路径预期输入产生预期输出add(2, 3) 返回 5
边界有效输入范围边缘的值空字符串、零、最大整数、单个元素
错误无效输入触发适当的异常需要 str 时传入 None、负索引
状态状态转换产生正确的副作用对象从 pending 转为 active

对于每个用例,记录:

  • 输入值(具体值,而非抽象描述)
  • 预期输出或异常
  • 所需的设置(fixture)
  • 所需的 mock(用于抑制外部调用)

对于测试逻辑相同、仅输入或输出值不同的用例,使用 parametrize。

第 3 阶段:夹具设计
  1. 识别共享设置 — 如果 3 个或更多测试需要同一个对象,就提取一个夹具。

  2. 选择作用域 — 使用能避免重复设置的最小作用域:

    作用域使用场景示例
    function默认。每个测试都会获得全新状态大多数单元测试
    class同一个类中的测试共享开销较大的设置每个测试类共用一个 DB 连接
    module同一个文件中的所有测试共享设置已加载的配置文件
    session整个测试运行过程共享设置Docker 容器启动
  3. 设计清理流程 — 需要清理时,使用 yield 夹具。测试结束后,绝不遗留 副作用(临时文件、DB 行、monkey-patch)。

  4. 识别 conftest 候选项 — 多个测试文件都会用到的夹具应放在 conftest.py 中。仅在一个文件中使用的夹具则留在该文件中。

第 4 阶段:模拟策略
  1. 确定要模拟的内容 — 仅模拟外部依赖:

    • 网络调用(API、数据库、消息队列)
    • 文件系统操作(测试逻辑而非 I/O 时)
    • 依赖时间的行为(datetime.now、time.sleep)
    • 随机或非确定性行为
  2. 确定不应模拟的内容 — 绝不模拟:

    • 被测函数
    • 被测目标调用的纯函数(通过被测目标对其进行测试)
    • 数据结构和值对象
  3. 选择模拟层级 — 在被测模块的导入边界处打补丁, 而不是在定义处。使用 @patch('mymodule.requests.get'),不要使用 @patch('requests.get')。

  4. 添加模拟断言 — 每个模拟都应断言它使用预期参数、按预期次数调用。没有断言的模拟会造成覆盖率盲区。

第 5 阶段:输出

按照以下结构生成测试文件:

  1. 导入项(pytest、模拟对象、目标模块)
  2. 常量和测试数据
  3. 夹具(按作用域排序:session > module > class > function)
  4. 按目标函数分组的测试类或函数
  5. 适用时使用参数化测试

输出格式

# 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模块或包所有类别 + 异步 + 参数化矩阵发布前、关键路径代码

校准规则

  1. 测试隔离不可妥协。 每个测试都必须单独运行时通过,也必须在任意顺序下通过。 任何测试都不得依赖其他测试产生的副作用。
  2. 严格管理 Mock。 Mock 外部依赖,而非内部逻辑。过度 Mock 会导致代码出错时测试仍然通过;Mock 不足则会导致网络中断时测试失败。
  3. 具体优于抽象。 测试数据必须使用具体值,而非占位符。使用 "[email protected]",而不是 "test_email";使用 42,而不是 "some_number"。具体值可以捕获抽象占位符掩盖的类型不匹配。
  4. 每个测试聚焦一项断言。 一个测试应验证一种行为。如果多个断言验证的是同一种行为的不同方面(例如返回值和副作用),则可以接受;但不要用它们验证无关行为。
  5. 使用参数化,而非重复测试。 如果两个测试的区别仅在于输入/输出值,请使用 @pytest.mark.parametrize 将它们合并。使用 ids 设置易读的测试名称。
  6. 遵循项目约定。 如果项目使用 conftest.py fixture、基于类的测试或特定标记,请遵循这些模式。不要引入与之冲突的测试风格。

错误处理

问题解决方法
目标函数没有类型注解根据用法模式、默认值和文档字符串推断类型。在测试文档字符串中注明不确定之处。
目标函数有深层嵌套的依赖项在最靠近被测函数的边界处进行 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、系统时钟、单元测试中的文件系统)

评论

支持 Markdown