--- name: tdd description: 测试驱动开发,采用红-绿-重构循环。当用户希望使用 TDD 构建功能或修复 Bug、提及"红-绿-重构"、需要集成测试,或要求测试优先开发时使用。 --- # 测试驱动开发 ## 核心理念 **核心原则**:测试应通过公共接口验证行为,而非实现细节。代码可以彻底改变,但测试不应受影响。 **好的测试**是集成风格的:它们通过公共 API 执行真实的代码路径。它们描述的是系统**做什么**,而不是**怎么做**。好的测试读起来就像一份规格说明——"用户可以使用有效购物车结账"精确地告诉你存在什么能力。这类测试能经受重构,因为它们不关心内部结构。 **差的测试**与实现紧密耦合。它们模拟内部协作者、测试私有方法,或通过外部手段验证(例如直接查询数据库而不是通过接口)。警示信号:重构时测试失败,但行为并未改变。如果你重命名了一个内部函数而测试失败,那些测试测试的是实现,而非行为。 参见 [tests.md](tests.md) 获取示例,以及 [mocking.md](mocking.md) 了解模拟指南。 ## 反模式:水平切片 **不要**先编写所有测试,再编写所有实现。这是"水平切片"——把 RED 阶段当作"编写所有测试",把 GREEN 阶段当作"编写所有代码"。 这会产生**糟糕的测试**: - 批量编写的测试测试的是**想象的**行为,而非**实际的**行为 - 你最终测试的是事物的**形状**(数据结构、函数签名),而非面向用户的行为 - 测试对真实变化不敏感——行为被破坏时它们通过,行为正常时它们却失败 - 你超出了自己的视野范围,在理解实现之前就确定了测试结构 **正确方法**:通过示踪弹头进行垂直切片。一个测试 → 一个实现 → 重复。每个测试都响应你在上一个周期中学到的东西。因为你刚刚编写了代码,所以确切知道什么行为重要以及如何验证它。 ``` 错误(水平切片): RED: test1, test2, test3, test4, test5 GREEN: impl1, impl2, impl3, impl4, impl5 正确(垂直切片): RED→GREEN: test1→impl1 RED→GREEN: test2→impl2 RED→GREEN: test3→impl3 ... ``` ## 工作流程 ### 1. 规划 在探索代码库时,使用项目的领域术语表,使测试名称和接口词汇与项目的语言保持一致,并尊重你所接触区域的架构决策记录(ADR)。 在编写任何代码之前: - [ ] 与用户确认需要哪些接口变更 - [ ] 与用户确认要测试哪些行为(确定优先级) - [ ] 识别适合[深度模块](deep-modules.md)的机会(小接口,深实现) - [ ] 设计[可测试性](interface-design.md)接口 - [ ] 列出要测试的行为(而非实现步骤) - [ ] 获得用户对计划的批准 询问:"公共接口应该是什么样子?哪些行为最重要需要测试?" **你无法测试所有东西。** 与用户确认哪些行为最重要。将测试精力集中在关键路径和复杂逻辑上,而非每一个可能的边界情况。 ### 2. 示踪弹头 编写一个测试,确认关于系统的一件事: ``` RED: 为第一个行为编写测试 → 测试失败 GREEN: 编写最简代码使其通过 → 测试通过 ``` 这就是你的示踪弹头——证明路径可以端到端工作。 ### 3. 增量循环 对每个剩余行为: ``` RED: 编写下一个测试 → 失败 GREEN: 最简代码使其通过 → 通过 ``` 规则: - 一次只写一个测试 - 只编写足够通过当前测试的代码 - 不要预先推测未来的测试 - 保持测试聚焦于可观察的行为 ### 4. 重构 所有测试通过后,寻找[重构候选](refactoring.md): - [ ] 提取重复代码 - [ ] 加深模块(将复杂度隐藏在简单接口之后) - [ ] 在自然的情况下应用 SOLID 原则 - [ ] 考虑新代码揭示了关于现有代码的什么信息 - [ ] 每次重构后运行测试 **永远不要在 RED 状态下重构。** 先回到 GREEN。 ## 每个周期的检查清单 ``` [ ] 测试描述的是行为,而非实现 [ ] 测试只使用公共接口 [ ] 测试在内部重构后仍能存活 [ ] 代码针对此测试是最简的 [ ] 没有添加推测性的功能 ```