关于 tdd-anti-patterns
面向代码审查和测试套件维护的参考目录,汇总测试反模式、识别症状及对应的 TDD 改进方法,帮助团队排查慢速、不稳定或难以理解的测试。
主要功能
- 01列出测试反模式及症状
- 02解释问题成因与影响
- 03关联对应的 TDD 改进技巧
- 04提供审查与排查判断原则
适用场景
- 01代码审查时检查测试代码
- 02梳理缓慢或不稳定的 CI 测试
- 03向新成员说明团队测试规范
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ yonatankarp/ software-design-skills --skill tdd-anti-patterns安装后在 Claude Code 中输入/tdd-anti-patterns确认已加载。
详细介绍
一句话概述
测试套件反模式参考目录。每种坏味道都包含可识别的症状、对其不妥之处的解释,以及解决它的 TDD 技术指引。
何时使用此技能
- 审查测试代码。
- 测试套件变得杂乱,需要有人判断先修复哪些问题。
- 新人培训:解释“我们这里不做 X,以及原因”。
- 诊断 CI 问题(速度慢、不稳定、难以调试)。
何时不使用此技能
- 还没编写测试——请直接使用对应技术的技能。
- 某个特定测试因合理原因而失败——这是调试,而非审计。
核心内容
测试相互依赖。 单独运行时测试通过,但一起运行时失败。症状: CI 在那个“本地总是通过”的测试上失败。解决方法: tdd-test-isolation。
测试套件运行缓慢。 单个测试耗时超过约 100 毫秒;整个测试套件要运行数小时。症状: 开发者不再在本地运行测试。解决方法: 将真实 I/O、真实时间和真实网络隔离在替身之后(tdd-mock-object);单元测试使用内存中的伪对象。
不稳定的测试。 同一个测试有时失败、有时通过,代码却没有变化。症状: 形成“重新运行构建”的习惯。原因: 竞态条件、真实时钟、非确定性顺序、网络。解决方法: 注入时钟、注入执行器、设定随机种子、隔离测试(tdd-test-isolation)。
神秘访客。 测试依赖它没有创建的数据,例如数据库行或磁盘上的文件。症状: 测试断言中出现 select * from users where id = 42,却没有记录说明 42 是怎么来的。解决方法: 由测试自行设置测试夹具。
过度模拟。 测试验证调用了哪些协作者,而不是产生了什么行为。症状: 到处都是 verify(mock, times(2));无害的重构就会导致十个测试失败。解决方法: 只在边界处使用模拟对象(tdd-mock-object);领域内部使用真实对象。
测试框架本身。 例如,验证 @Transactional 是否回滚,或 Jackson 是否能序列化 LocalDate 的测试。症状: 即使团队的代码不存在,测试也会通过。解决方法: 删除它;框架已经测试过自身。
测试实现,而非行为。 测试断言内部状态(私有字段值、方法调用次数),而不是可观察的结果。症状: 不改变行为的重构也会导致数十个测试失败。解决方法: 断言输出、返回值和对外部产生的副作用,而非内部细节。
模拟不归你所有的对象。 测试直接模拟第三方 SDK 的类。症状: SDK 升级后,模拟对象的行为悄然改变;测试通过,生产环境却出问题。解决方法: 在一个由你控制的小型接口后封装 SDK;模拟该接口;为 SDK 添加一个 tdd-learning-test。
修复 Bug 时没有添加测试。 修复 Bug 时没有添加回归测试。症状: 几周后 Bug 再次出现。解决方法: tdd-regression-test——每次修复 Bug 都要添加测试。
测试过于庞大。 单个测试设置了 50 行测试夹具,验证 10 种行为,并断言 20 个结果。症状: 测试失败时,没人知道原因。解决方法: 拆分为小型且专注的测试;默认每个测试只写一个断言。
测试代码未经代码审查。 测试代码被视为次等代码——不审查、不重构,也不遵循命名规范。症状: 测试套件比生产代码还杂乱。解决方法: 像对待生产代码一样对待测试代码。测试套件是设计的可执行规范。
模拟自己的数据类。 明明可以用 Order(...),却使用 mock<Order>()。症状: 必须告知模拟对象它拥有的每个属性。解决方法: 构造真实的值对象。
断言隐藏在辅助方法中。 测试调用 verifyEverything(...);实际断言藏在三层调用之下。症状: 失败消息只有“AssertionError”,没有任何上下文。解决方法: 断言应放在测试方法中;辅助方法应设置状态,而不是进行断言。
用随机休眠来同步。 使用 Thread.sleep(2000) 来“等待异步操作完成”。症状: 测试变慢且不稳定,而这两个问题都没有真正解决。解决方法: 使用适当的同步原语、等待 Future;对挂起代码使用 kp-testing-coroutines 虚拟时间。
断言没有消息。 使用 assertEquals(expected, actual),却没有第三个参数作为消息。症状: CI 失败时只显示两个数字,完全看不出检查的是什么。解决方法: 添加断言消息或逐行说明。
判定启发
- 严重程度取决于影响范围:CI 中的慢速测试会影响每位开发者;单个文件中的一个脆弱测试则是小问题。
- 反复运行不稳定的测试,直到它通过,这不算修复。要找出不确定性的来源。
- 为了“让测试更快”而添加的模拟,是最常见的过度模拟入门方式。要审视每一个新模拟。
- 测试也是文档:测试的名称和结构应当说明代码的行为。如果没有做到,那么无论测试通过与否,这个测试都是有问题的。
关于发现反模式的反模式
- 把所有长测试都视为坏测试——有时集成测试确实会比较长。
- 要求完全不使用模拟——边界模拟是正确且必要的。
- 不重新运行测试就为了“整洁”而重构测试。测试代码也是代码;同样的规范也适用于它。
另请参阅
- 其他全部十一个
tdd-*技能——每项修复都会引用相关技术。 kp-testing-coroutines——与协程相关的测试异味。kp-anti-patterns——Kotlin 语言异味(也常出现在测试代码中)。
参考资料
- Kent Beck,《Test-Driven Development By Example》,第 27 章——书中在介绍模式的同时,也提到了许多测试反模式。
- Gerard Meszaros,《xUnit Test Patterns》——详尽的异味目录(权威参考资料)。
- Robert C. Martin,《Clean Code》,第 9 章——整洁的测试;FIRST 原则(快速、独立、可重复、自我验证、及时)。