TDD 的重大误解(2022)
本文审视了测试驱动开发(TDD)和单元测试,许多人认为与实现细节紧密耦合的测试会让测试套件变得脆弱,妨碍重构并浪费精力。评论者将低层单元测试与更高层的集成测试和端到端测试进行对比,讨论最佳性价比在哪里、覆盖率究竟有多大用处,以及测试是否应主要从用户或系统边界来验证行为。其背后是对教条式方法论的更广泛担忧:有人认为 TDD 在被审慎应用时是强大的设计辅助,而另一些人则把它视为一种需要根据上下文、质量目标和系统复杂度来选择的情境化工具。
单元测试中“unit”的含义
- 文章声称“unit”最初指的是一个隔离测试,这一点遭到强烈质疑。
- 几位评论者引用了更早的定义(例如“可编译的小模块,约 100 行代码”),其中 unit 显然指的是被测试的代码,而不是测试本身。
- 线程中的共识是:语言含义已经漂移,但大多数实践者和历史文档都把 unit 视为一个代码组件(函数、类、模块),不过也有人接受“独立测试”作为一种有用的现代含义。
TDD 是关于测试还是设计?
- 一种观点:最大的误解是把 TDD 当成测试;它主要是一种设计实践,借助测试来塑造 API 和职责分离。
- 相反的观点:把 TDD 视作设计被过分夸大,甚至有害;设计能力和判断力更重要,而 TDD 提供的设计指导很弱,尤其对经验较少的开发者更是如此。
- 也有人将这个术语拆分:Test-Driven Development 与 Test-Driven Design,认为它们常常被混为一谈。
单元测试 vs 集成/E2E 测试
- 许多人认为更高层的功能/集成测试价值更大:
- 更接近用户可见行为。
- 在重构过程中更稳定。
- 避免测试过度耦合实现,以及大量 mock 带来的混乱。
- 也有人为传统测试金字塔辩护:大量快速、隔离的单元测试能捕捉边界情况和缺陷,而更广泛的测试可能会遗漏这些问题,尤其是在复杂算法或库中。
- 常见折中方案:
- 对纯粹、复杂的逻辑或可复用基础设施使用单元测试。
- 对业务流程以及 HTTP/JSON 或 UI 边界使用集成/E2E 测试。
脆弱的测试与重构痛点
- 一个常见抱怨是:在“不改变行为”的情况下重构,会破坏许多低层测试。
- 原因分析:测试过于白盒、过度 mock,或者断言了无关的内部细节。
- 提到的策略包括:
- 让测试聚焦于外部可观察行为。
- 接受某些早期、偏实现的测试在更高层行为测试出现后应当被删除或演进。
- 将测试更多地视为重构的安全护栏,而不是设计约束。
教条主义、工作流与指标
- 关于“从不在没有红灯测试的情况下改代码”的争论:有人认为这是 TDD 的核心,也有人认为这只是多余的仪式感,尤其在探索性工作中更是如此。
- 对教条式 TDD、强制 100% 覆盖率,以及把覆盖率当 KPI 的做法普遍持怀疑态度(Goodhart 定律)。
- 几位评论者强调情境:领域风险、语言特性(动态类型 vs 强类型)、非功能性需求以及团队技能都应该决定测试策略,而不是某种单一的通用方法论。