关于 test-driven-development
指导编码 Agent 严格采用测试驱动开发:先编写失败测试,再实现最少代码,随后重构。适用于功能开发、修复缺陷、重构和行为变更;规定的例外需先获用户同意。
主要功能
- 01遵循红绿重构循环
- 02确认测试因预期原因失败
- 03编写通过测试所需的最少代码
- 04运行指定测试及完整测试套件
- 05检查测试优先流程的反模式
适用场景
- 01按测试优先步骤添加功能
- 02复现缺陷并防止回归
- 03在测试持续通过时重构
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ nousresearch/ hermes-agent --skill test-driven-development安装后在 Claude Code 中输入/test-driven-development确认已加载。
详细介绍
测试驱动开发(TDD)
概述
先编写测试,观察它失败,再编写最精简的代码使其通过。
核心原则: 如果你没有亲眼看到测试失败,就不知道它是否测试了正确的内容。
违反规则的字面要求,就是违背规则的精神。
何时使用
始终适用:
- 新功能
- Bug 修复
- 重构
- 行为变更
例外情况(先询问用户):
- 一次性原型
- 生成的代码
- 配置文件
在想“这次就跳过 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,而不是真实代码。
要求:
- 每个测试只验证一种行为
- 名称清晰且有描述性(名称里有“和”?拆开)
- 测试真实代码,而非 Mock(确实无法避免时除外)
- 名称描述行为,而非实现
验证 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 — 清理代码
只有测试通过后才能进行:
- 消除重复
- 改进命名
- 提取辅助函数
- 简化表达式
始终确保测试通过。不要添加新行为。
重构期间测试失败? 立即撤销。缩小每次改动的范围。
重复
为下一个行为编写下一个失败的测试。一次只进行一个循环。
避免水平切片
不要先编写所有测试,再编写所有实现。这就是水平切片:RED 变成“编写一堆想象出来的测试”,GREEN 变成“让这堆测试通过”。这种做法会产生脆弱的测试,因为在实现尚未让你了解哪些行为和接口真正重要之前,测试就已经设计好了。
改用纵向示踪弹:
WRONG:
RED: test1, test2, test3, test4
GREEN: impl1, impl2, impl3, impl4
RIGHT:
RED→GREEN: test1→impl1
RED→GREEN: test2→impl2
RED→GREEN: test3→impl3
示踪弹是一个端到端的行为切片。它能证明整条路径可行,让你了解接口,并确保接下来的每个测试都以刚刚获得的经验为基础。
顺序为何重要
“我写完后再补测试来验证是否正常工作。”
代码写完后再写的测试会立即通过。立即通过并不能证明任何事:
- 可能测错了对象
- 可能测的是实现,而不是行为
- 可能漏掉了你忘记考虑的边界情况
- 你从未看到它捕获过 bug
先写测试会迫使你看到测试失败,从而证明它确实测到了问题。
“我已经手动测试过所有边界情况了。”
手动测试是临时性的。你以为自己测全了,但:
- 没有记录测过什么
- 代码变更后无法重新运行
- 压力之下很容易漏掉情况
- “我试过时能用” ≠ 全面测试
自动化测试是系统性的。每次运行的方式都相同。
“删除 X 小时的工作太浪费了。”
这是沉没成本谬误。时间已经花掉了。现在你可以选择:
- 删除并用 TDD 重写(信心充足)
- 保留代码,之后再补测试(信心不足,很可能有 bug)
真正的“浪费”,是保留你无法信任的代码。
“TDD 很教条,务实就该灵活变通。”
TDD 很务实:
- 在提交前发现 bug(比事后调试更快)
- 防止回归(测试会立即发现问题)
- 记录行为(测试展示如何使用代码)
- 支持重构(可以自由修改,测试会发现问题)
“务实”的捷径 = 在生产环境中调试 = 更慢。
“事后写测试也能达到同样的目标——重要的是精神,不是仪式。”
不对。事后写的测试回答“这段代码做什么?”,先写测试回答“这段代码应该做什么?”
事后写的测试会受到实现方式的影响。你测试的是自己构建的内容,而不是实际需求。先写测试会迫使你在实现前找出边界情况。
常见的自我辩解
| 借口 | 事实 |
|---|---|
| “太简单了,不值得测” | 简单代码也会出错。测试只需 30 秒。 |
| “我之后再测” | 测试立即通过并不能证明任何事。 |
| “事后写测试也能达到同样的目标” | 事后测试 =“这段代码做什么?”先写测试 =“这段代码应该做什么?” |
| “我已经手动测试过了” | 临时测试 ≠ 系统测试。没有记录,也无法重新运行。 |
| “删除 X 小时的工作太浪费了” | 沉没成本谬误。保留未经验证的代码就是技术债务。 |
| “先留着作参考,再先写测试” | 你会基于它调整。这就是事后测试。删除就是删除。 |
| “我需要先探索一下” | 可以。丢弃探索代码,从 TDD 开始。 |
| “测试很难写 = 设计不清晰” | 听听测试的反馈。难以测试 = 难以使用。 |
| “TDD 会拖慢我” | TDD 比调试更快。务实做法 = 先写测试。 |
| “手动测试更快” | 手动测试无法证明边界情况都覆盖了。每次改动后你都得重新测试。 |
| “现有代码没有测试” | 你是在改进代码。为你改动的代码添加测试。 |
危险信号——停止并重新开始
如果你发现自己在做以下任何一件事,就删除代码并用 TDD 重新开始:
- 先写代码再写测试
- 实现之后才写测试
- 测试第一次运行就通过
- 无法解释测试为何失败
- 测试“以后再加”
- 为“就这一次”找理由
- “我已经手动测试过了”
- “事后写测试也能达到同样的目的”
- “留着作参考”或“基于现有代码调整”
- “已经花了 X 小时,删除太浪费了”
- “TDD 很教条,我只是在务实变通”
- “这次情况不同,因为……”
这些都意味着:删除代码。从 TDD 重新开始。
验证清单
将工作标记为完成前:
- 每个新增的函数/方法都有测试
- 在实现之前观察到每个测试失败
- 每个测试都因预期原因失败(缺少功能,而不是拼写错误)
- 编写最少的代码,让每个测试通过
- 所有测试都通过
- 输出干净(没有错误或警告)
- 测试使用真实代码(仅在无法避免时使用模拟)
- 覆盖边界情况和错误
无法勾选所有项?说明你跳过了 TDD。重新开始。
遇到困难时
| 问题 | 解决方案 |
|---|---|
| 不知道如何测试 | 写出期望的 API。先写断言。询问用户。 |
| 测试过于复杂 | 设计过于复杂。简化接口。 |
| 必须模拟所有内容 | 代码耦合过多。使用依赖注入。 |
| 测试设置繁琐 | 提取辅助函数。仍然复杂?简化设计。 |
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
为实现任务分派子 Agent 时,在目标中强制要求采用 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?编写失败的测试来复现它。遵循 TDD 循环。测试可以验证修复,并防止回归。
修复 Bug 时,绝不跳过测试。
测试反模式
- 测试模拟行为而非真实行为 — 模拟应验证交互,而不是替代被测系统
- 测试实现细节 — 测试行为或结果,而不是内部方法调用
- 只测试正常路径 — 始终测试边缘情况、错误和边界条件
- 脆弱的测试 — 测试应验证行为,而不是结构;重构不应导致测试失败
最终规则
Production code → test exists and failed first
Otherwise → not TDD
未经用户明确许可,不得破例。