跳到主要内容

测试驱动开发

在编写实现代码之前,在实现任何功能或修复错误时使用。通过测试优先的方法强制执行 RED-GREEN-REFACTOR(红-绿-重构)循环。

技能元数据

来源捆绑(默认安装)
路径skills/software-development/test-driven-development
版本1.1.0
作者Hermes Agent(改编自 obra/superpowers)
许可证MIT
标签testing, tdd, development, quality, red-green-refactor
相关技能systematic-debugging, writing-plans, subagent-driven-development

参考:完整 SKILL.md

信息

以下是 Hermes 在触发此技能时加载的完整技能定义。这是技能激活时代理看到的指令。

测试驱动开发 (TDD)

概述

先写测试。观察其失败。编写最小化的代码以通过测试。

核心原则: 如果你没有观察到测试失败,你就不知道它是否测试了正确的内容。

违反规则的字面意思就是违反规则的精神实质。

何时使用

始终使用:

  • 新功能
  • 错误修复
  • 重构
  • 行为变更

例外情况(请先询问用户):

  • 一次性原型
  • 生成的代码
  • 配置文件

想着“就这一次跳过 TDD”?停下。那是自我合理化。

铁律

NO PRODUCTION CODE WITHOUT A FAILING TEST FIRST

在测试之前编写代码?删除它。重新开始。

绝无例外:

  • 不要将其保留为“参考”
  • 不要在编写测试时“调整”它
  • 不要查看它
  • 删除意味着彻底删除

根据测试从头实现。仅此而已。

Red-Green-Refactor 循环

RED — 编写失败的测试

编写一个展示预期行为的最小化测试。

好的测试:

def test_retries_failed_operations_3_times():
attempts = 0
def operation():
nonlocal attempts
attempts += 1
if attempts < 3:
raise Exception('fail')
return 'success'

result = retry_operation(operation)

assert result == 'success'
assert attempts == 3

名称清晰,测试真实行为,只测一件事。

坏的测试:

def test_retry_works():
mock = MagicMock()
mock.side_effect = [Exception(), Exception(), 'success']
result = retry_operation(mock)
assert result == 'success' # What about retry count? Timing?

名称模糊,测试的是 mock 而非真实代码。

要求:

  • 每个测试只针对一种行为
  • 清晰描述性的名称(名称中有“and”?将其拆分)
  • 真实代码,而非 mocks(除非确实不可避免)
  • 名称描述行为,而非实现细节

验证 RED — 观察其失败

强制要求。绝不可跳过。

# Use terminal tool to run the specific test
pytest tests/test_feature.py::test_specific_behavior -v

确认:

  • 测试失败(而非因拼写错误导致的报错)
  • 失败消息符合预期
  • 失败是因为功能缺失

测试立即通过? 你正在测试现有行为。修正测试。

测试报错? 修复错误,重新运行直到它正确失败。

GREEN — 最小化代码

编写通过测试的最简单代码。仅此而已,不多不少。

好的做法:

def add(a, b):
return a + b # Nothing extra

坏的做法:

def add(a, b):
result = a + b
logging.info(f"Adding {a} + {b} = {result}") # Extra!
return result

不要添加功能、重构其他代码或进行超出测试范围的“改进”。

在 GREEN 阶段作弊是可以接受的:

  • 硬编码返回值
  • 复制粘贴
  • 重复代码
  • 忽略边缘情况

我们将在 REFACTOR 阶段修复这些问题。

验证 GREEN — 观察其通过

强制要求。

# Run the specific test
pytest tests/test_feature.py::test_specific_behavior -v

# Then run ALL tests to check for regressions
pytest tests/ -q

确认:

  • 测试通过
  • 其他测试仍然通过
  • 输出干净(无错误、无警告)

测试失败? 修复代码,而不是修复测试。

其他测试失败? 立即修复回归问题。

REFACTOR — 清理

仅在变绿(Green)之后:

  • 消除重复
  • 改进命名
  • 提取辅助函数
  • 简化表达式

在整个过程中保持测试通过。不要添加新行为。

如果在重构期间测试失败: 立即撤销。采取更小的步骤。

重复

针对下一个行为编写下一个失败的测试。一次一个循环。

为什么顺序很重要

“我会在之后编写测试来验证它是否有效”

在代码之后编写的测试会立即通过。立即通过证明不了什么:

  • 可能测试了错误的内容
  • 可能测试的是实现细节,而非行为
  • 可能遗漏了你忘记的边缘情况
  • 你从未看到它捕获错误

测试优先迫使您看到测试失败,从而证明它确实在测试某些内容。

“我已经手动测试了所有边缘情况”

手动测试是随意的。你认为你测试了一切,但:

  • 没有记录你测试了什么
  • 代码更改时无法重新运行
  • 在压力下容易遗漏情况
  • “我试的时候它是有效的” ≠ 全面覆盖

自动化测试是系统化的。它们每次都以相同的方式运行。

“删除 X 小时的工作成果是浪费”

这是沉没成本谬误。时间已经流逝。你现在的选择是:

  • 删除并使用 TDD 重写(高置信度)
  • 保留它并在之后添加测试(低置信度,可能存在错误)

真正的“浪费”是保留你无法信任的代码。

“TDD 是教条主义的,务实意味着适应”

TDD 本身就是务实的:

  • 在提交前发现错误(比事后调试更快)
  • 防止回归(测试能立即捕获破坏)
  • 记录行为(测试展示如何使用代码)
  • 支持重构(自由更改,测试能捕获破坏)

所谓的“务实”捷径 = 在生产环境中调试 = 更慢。

“事后测试也能达到相同的目标——重要的是精神而非形式”

否。后写测试回答的是“这段代码做了什么?” 先写测试回答的是“这段代码应该做什么?”

后写测试会受到你具体实现的偏见影响。你测试的是你构建的东西,而不是需求所要求的东西。先写测试迫使你在实现之前发现边界情况。

常见的合理化借口

借口现实
“太简单了,不用测试”简单的代码也会出错。测试只需 30 秒。
“我稍后再测试”测试立即通过证明不了任何东西。
“后写测试能达到相同的目标”后写测试 = “这段代码做了什么?” 先写测试 = “这段代码应该做什么?”
“已经手动测试过了”临时测试 ≠ 系统测试。没有记录,无法重新运行。
“删除 X 小时的工作成果是浪费”沉没成本谬误。保留未经验证的代码就是技术债务。
“保留作为参考,先写测试”你会去适配它。那就是后写测试。删除意味着彻底删除。
“需要先探索一下”没问题。丢弃探索性代码,从 TDD 开始。
“测试难写 = 设计不清晰”倾听测试的声音。难以测试 = 难以使用。
“TDD 会拖慢我的速度”TDD 比调试更快。务实的做法是先写测试。
“手动测试更快”手动测试无法证明边界情况。每次更改你都得重新测试。
“现有代码没有测试”你正在改进它。为你触动的代码添加测试。

危险信号 — 停止并重新开始

如果你发现自己有以下任何行为,请删除代码并使用 TDD 重新开始:

  • 先写代码后写测试
  • 在实现之后编写测试
  • 测试在首次运行时立即通过
  • 无法解释测试失败的原因
  • “稍后”才添加测试
  • 合理化“就这一次”
  • “我已经手动测试过了”
  • “后写测试能达到相同的目的”
  • “保留作为参考”或“适配现有代码”
  • “已经花了 X 小时,删除太浪费了”
  • “TDD 太教条,我是在务实行事”
  • “这种情况不同,因为……”

所有这些均意味着:删除代码。使用 TDD 重新开始。

验证清单

在标记工作完成之前:

  • 每个新函数/方法都有测试
  • 在实现之前观察到每个测试失败
  • 每个测试都因预期原因失败(缺少功能,而非拼写错误)
  • 编写了通过每个测试所需的最小化代码
  • 所有测试均通过
  • 输出干净(无错误、无警告)
  • 测试使用真实代码(仅在不可避免时使用 mock)
  • 覆盖了边界情况和错误

无法勾选所有选项?你跳过了 TDD。重新开始。

遇到困境时

问题解决方案
不知道如何测试写出你期望的 API。先编写断言。询问用户。
测试过于复杂设计过于复杂。简化接口。
必须 mock 所有内容代码耦合度过高。使用依赖注入。
测试设置庞大提取辅助函数。仍然复杂?简化设计。

Hermes Agent 集成

运行测试

在每一步使用 terminal 工具运行测试:

# RED — verify failure
terminal("pytest tests/test_feature.py::test_name -v")

# GREEN — verify pass
terminal("pytest tests/test_feature.py::test_name -v")

# Full suite — verify no regressions
terminal("pytest tests/ -q")

配合 delegate_task

在分派子代理进行实现时,在目标中强制要求 TDD:

delegate_task(
goal="Implement [feature] using strict TDD",
context="""
Follow test-driven-development skill:
1. Write failing test FIRST
2. Run test to verify it fails
3. Write minimal code to pass
4. Run test to verify it passes
5. Refactor if needed
6. Commit

Project test command: pytest tests/ -q
Project structure: [describe relevant files]
""",
toolsets=['terminal', 'file']
)

配合 systematic-debugging

发现 Bug?编写一个复现该 Bug 的失败测试。遵循 TDD 循环。该测试证明了修复的有效性并防止回归。

切勿在没有测试的情况下修复 Bug。

测试反模式

  • 测试 mock 行为而非真实行为 — mock 应用于验证交互,而非替代被测系统
  • 测试实现细节 — 测试行为/结果,而非内部方法调用
  • 仅测试正常路径 — 始终测试边界情况、错误和边界条件
  • 脆弱的测试 — 测试应验证行为,而非结构;重构不应导致测试失败

最终规则

Production code → test exists and failed first
Otherwise → not TDD

未经用户明确许可,不得有任何例外。