测试驱动开发
TDD:强制执行 RED-GREEN-REFACTOR,先写测试再写代码
Test-Driven Development (TDD)
概述
先写测试,看着它失败,写最少的代码让它通过。
核心原则:如果你没有看到测试失败,你不知道它是否在测试正确的东西。
违反规则的字面意思就是违反规则的精神。
使用场景
始终使用:
例外(先问用户):
想"就跳过 TDD 这一次"?停止。这是合理化。
铁律
没有失败的测试就不能写生产代码
在测试之前写代码?删除它。重新开始。
没有例外:
从测试开始重新实现。就这样。
红-绿-重构循环
红色 — 编写失败的测试
写一个最小的测试,展示应该发生的事情。
好的测试:
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' # 那重试次数呢?时间呢?
模糊的名称,测试 mock 而不是真实代码。
要求:
验证红色 — 看着它失败
强制。绝不跳过。
# 使用终端工具运行特定测试
pytest tests/test_feature.py::test_specific_behavior -v
确认:
测试立即通过? 你在测试现有行为。修复测试。
测试错误? 修复错误,重新运行直到它正确失败。
绿色 — 最少代码
写最简单的代码让测试通过。不要更多。
好:
def add(a, b):
return a + b # 没有额外的东西
坏:
def add(a, b):
result = a + b
logging.info(f"Adding {a} + {b} = {result}") # 额外!
return result
不要添加功能,不要重构其他代码,不要在测试之外"改进"。
在绿色中作弊是允许的:
我们会在重构中修复它。
验证绿色 — 看着它通过
强制。
# 运行特定测试
pytest tests/test_feature.py::test_specific_behavior -v
# 然后运行所有测试检查回归
pytest tests/ -q
确认:
测试失败? 修复代码,而不是测试。
其他测试失败? 现在修复回归。
重构 — 清理
只有在绿色之后:
在整个过程中保持测试绿色。不要添加行为。
如果测试在重构期间失败: 立即撤销。采取更小的步骤。
重复
下一个失败测试用于下一个行为。一次一个循环。
为什么顺序重要
"我会在之后写测试来验证它是否工作"
代码之后写的测试立即通过。立即通过不能证明什么:
测试优先迫使你看到测试失败,证明它实际上测试了一些东西。
"我已经手动测试了所有边界情况"
手动测试是临时的。你认为你测试了所有东西但:
自动化测试是系统性的。它们每次以相同的方式运行。
"删除 X 小时的工作是浪费的"
沉没成本谬误。时间已经过去了。你现在的选择:
"浪费"是保留你无法信任的代码。
"TDD 是教条的,适应它意味着实用"
TDD 是实用的:
"实用"快捷方式 = 在生产中调试 = 更慢。
"之后测试实现相同目标 — 这是精神而不是仪式"
不是的。之后的测试回答"这做什么?"测试优先回答"这应该做什么?"
之后的测试受你的实现偏见。你测试你构建的,而不是需要的。测试优先强制在实现之前发现边界情况。
常见合理化
| 借口 | 现实 |
| "太简单不需要测试" | 简单代码会坏。测试需要 30 秒。 |
| "我之后测试" | 测试立即通过不能证明什么。 |
| "之后测试实现相同目标" | 之后测试 = "这做什么?"测试优先 = "这应该做什么?" |
| "已经手动测试了" | 临时 不等于 系统化。没有记录,无法重新运行。 |
| "删除 X 小时是浪费的" | 沉没成本谬误。保留未经验证的代码是技术债务。 |
| "保留作为参考,先写测试" | 你会改编它。那是之后测试。删除意味着删除。 |
| "需要先探索" | 可以。抛弃探索,用 TDD 开始。 |
| "测试难 = 设计不清晰" | 听测试的。难测试 = 难使用。 |
| "TDD 会让我变慢" | TDD 比调试更快。实用 = 测试优先。 |
| "手动测试更快" | 手动不能证明边界情况。每次更改你都会重新测试。 |
| "现有代码没有测试" | 你在改进它。为你接触的代码添加测试。 |
红旗 — 停止并重新开始
如果你发现自己做任何这些,删除代码并用 TDD 重新开始:
所有这些意味着:删除代码。用 TDD 重新开始。
验证清单
在标记工作完成之前:
无法勾选所有框?你跳过了 TDD。重新开始。
遇到困难时
| 问题 | 解决方案 |
| 不知道如何测试 | 写出你希望的 API。先写断言。问用户。 |
| 测试太复杂 | 设计太复杂。简化接口。 |
| 必须 mock 一切 | 代码太耦合。使用依赖注入。 |
| 测试设置巨大 | 提取辅助函数。还是复杂?简化设计。 |
Hermes Agent 集成
运行测试
在每一步使用终端工具运行测试:
# 红色 — 验证失败
terminal("pytest tests/test_feature.py::test_name -v")
# 绿色 — 验证通过
terminal("pytest tests/test_feature.py::test_name -v")
# 完整套件 — 验证无回归
terminal("pytest tests/ -q")
与 delegate_task 一起使用
当分派子代理进行实现时,在目标中强制 TDD:
delegate_task(
goal="使用严格 TDD 实现 [功能]",
context="""
遵循 test-driven-development skill:
1. 先写失败的测试
2. 运行测试验证它失败
3. 写最少的代码让测试通过
4. 运行测试验证它通过
5. 如需要重构
6. 提交
项目测试命令:pytest tests/ -q
项目结构:[描述相关文件]
""",
toolsets=["terminal", "file"]
)
与 systematic-debugging 一起使用
找到 bug?写重现它的失败测试。遵循 TDD 循环。测试证明修复并防止回归。
永远不要在没有测试的情况下修复 bug。
测试反模式
最终规则
生产代码 → 测试存在且先失败
否则 → 不是 TDD
没有用户的明确许可不能有例外。
安装指南
复制下方命令,在终端运行即可安装:
使用指南
安装完成后,在对话框中直接使用此技能。