mcpField
T
tdd-anti-patterns

tdd-anti-patterns

github.com/yonatankarp测试与质量139 次安装23 周前更新
代码仓库

关于 tdd-anti-patterns

面向代码审查和测试套件维护的参考目录,汇总测试反模式、识别症状及对应的 TDD 改进方法,帮助团队排查慢速、不稳定或难以理解的测试。

主要功能

  1. 01列出测试反模式及症状
  2. 02解释问题成因与影响
  3. 03关联对应的 TDD 改进技巧
  4. 04提供审查与排查判断原则

适用场景

  1. 01代码审查时检查测试代码
  2. 02梳理缓慢或不稳定的 CI 测试
  3. 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 原则(快速、独立、可重复、自我验证、及时)。

评论

支持 Markdown