先写测试,看它失败,再写最少的代码让它通过。
核心原则:如果你没有亲眼看到测试失败,你就不知道它是否测试了正确的东西。
违反规则的字面意思,就是违反规则的精神。
始终使用:
例外情况(需先询问用户):
在想“就这一次跳过 TDD”?停下来。那是自我合理化。
NO PRODUCTION CODE WITHOUT A FAILING TEST FIRST
在测试之前写了代码?删掉它,从头再来。
没有例外:
从测试出发全新实现。就这么简单。
编写一个最小的测试,展示应该发生什么。
好的测试:
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 而不是真实代码。
要求:
强制要求。绝不跳过。
# Use terminal tool to run the specific test
pytest tests/test_feature.py::test_specific_behavior -v
确认:
测试立即通过了?说明你在测试已有的行为。修正这个测试。
测试报错了?修复错误,重新运行,直到它以正确的方式失败。
编写能让测试通过的最简单的代码。仅此而已。
好的:
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 阶段修复它。
强制要求。
# Run the specific test
pytest tests/test_feature.py::test_specific_behavior -v
# Then run ALL tests to check for regressions
pytest tests/ -q
确认:
测试失败了?修复代码,而不是修改测试。
其他测试失败了?立即修复回归问题。
只有在变绿之后:
全程保持测试通过。不要添加新行为。
如果重构过程中测试失败:立即撤销。采用更小的步子。
为下一个行为编写下一个失败的测试。一次一个循环。
“我会在写完代码后再写测试来验证它是否有效”
代码之后写的测试会立即通过。立即通过什么也证明不了:
测试先行迫使你看到测试失败,从而证明它确实在测试某些东西。
“我已经手动测试过所有边界情况了”
手动测试是临时的、随意的。你以为你测过了一切,但是:
自动化测试是系统化的。它们每次都以相同的方式运行。
“删掉 X 小时的工作太浪费了”
这是沉没成本谬误。时间已经花掉了。你现在的选择是:
真正的“浪费”是保留你无法信任的代码。
“TDD 太教条了,务实意味着灵活变通”
TDD 本身就是务实的:
“务实”的捷径 = 在生产环境中调试 = 更慢。
“事后写测试也能达到同样的目的——重要的是精神而非仪式”
不。事后测试回答的是“这是什么?”测试先行回答的是“这应该是什么?”
事后测试会被你的实现所带偏。你测试的是你写了什么,而不是需要什么。测试先行迫使你在实现之前发现边界情况。
| 借口 | 现实 |
|--------|---------|
| “太简单了不用测试” | 简单的代码也会出错。写个测试只要 30 秒。 |
| “我稍后再测” | 测试立即通过什么也证明不了。 |
| “事后测试也能达到同样目的” | 事后测试 = “这是干什么的?”测试先行 = “这应该干什么?” |
| “已经手动测试过了” | 随意测试 ≠ 系统化测试。没有记录,无法重跑。 |
| “删掉 X 小时的工作太浪费” | 沉没成本谬误。保留未经验证的代码才是技术债。 |
| “留作参考,重新写测试” | 你会不自觉地照抄它。那就是事后测试。删除就是删除。 |
| “需要先探索一下” | 可以。把探索的代码扔掉,从 TDD 开始。 |
| “测试很难写 = 设计不清晰” | 听测试的话。难以测试 = 难以使用。 |
| “TDD 会拖慢我” | TDD 比调试快。务实 = 测试先行。 |
| “手动测试更快” | 手动测试无法证明边界情况。每次改动你都得重测。 |
| “现有代码没有测试” | 你正在改进它。为你接触到的代码添加测试。 |
如果你发现自己在做以下任何事情,删掉代码并用 TDD 重来:
以上任何一条都意味着:删除代码,用 TDD 从头再来。
在标记工作完成之前:
无法勾选所有选项?说明你跳过了 TDD。从头再来。
| 问题 | 解决方案 |
|---------|----------|
| 不知道怎么测试 | 先写你期望的 API。先写断言。询问用户。 |
| 测试太复杂 | 设计太复杂。简化接口。 |
| 必须 mock 一切 | 代码耦合过重。使用依赖注入。 |
| 测试准备工作庞大 | 提取辅助函数。仍然复杂?简化设计。 |
使用 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")
在派发子代理执行实现任务时,在目标中强制要求 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']
)
发现 bug 了?编写一个能复现它的失败测试。遵循 TDD 循环。这个测试能证明修复有效并防止回归。
绝不修复没有测试的 bug。
Production code → test exists and failed first
Otherwise → not TDD
没有用户的明确许可,不得有任何例外。
评论区