mcpField
T
tdd-mock-object

tdd-mock-object

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

关于 tdd-mock-object

面向 JVM 测试的技能,说明如何用测试替身隔离缓慢、外部或非确定性依赖。强调优先使用高保真替身、在边界处模拟,并避免过度模拟。

主要功能

  1. 01区分 Dummy、Fake、Stub、Spy 和 Mock
  2. 02提供替身选择启发
  3. 03说明如何隔离外部依赖
  4. 04强调契约测试与避免过度模拟

适用场景

  1. 01测试依赖数据库、网络或文件系统的代码
  2. 02隔离时间、随机数等非确定性输入
  3. 03验证通知等协作者是否被正确调用

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/yonatankarp/software-design-skills --skill tdd-mock-object

安装后在 Claude Code 中输入/tdd-mock-object确认已加载。

详细介绍

一句话概述

使用测试替身,替代那些速度太慢、过度依赖外部环境或非确定性太强、无法在测试中调用的真实协作者。谨慎使用——过度模拟会让测试依赖实现细节,而非行为。

何时使用此技能

  • 否则,测试会访问数据库、网络、文件系统、消息代理或其他任何“JVM 之外”的资源。
  • 否则,测试会依赖系统时间、随机性或其他非确定性来源。
  • 否则,测试会调用速度慢、收费或有速率限制的第三方 API。
  • 你想验证协作者是否被正确调用(例如,是否触发了通知器)。

何时不使用此技能

  • 依赖速度快、行为确定且在进程内——直接使用真实对象即可。
  • 你模拟的是自己编写、且其行为正是测试对象的类。这是在测试模拟对象,而非代码。
  • 你模拟的是值对象或数据结构——这没有必要,直接构造真实对象即可。
  • 你把所有东西都模拟了,测试现在大部分内容都是模拟设置。应先简化设计,再添加更多模拟。

核心内容

五种类型(Meszaros 的分类法,广泛使用):

  • Dummy(占位对象)。 传入但不会实际使用真实值时使用的占位对象。(Kotlin 中的 null、Object()、Any()。)
  • Fake(伪实现)。 可用但经过简化的实现。保存 MutableMap<Id, Entity> 的内存中 Repository 就是伪实现。
  • Stub(桩)。 返回预设的答案;不验证调用方式。“请求 findById(42) 时,返回这个预设的 User。”
  • Spy(间谍)。 会记录调用方式的桩,因此测试可以断言调用情况。
  • Mock(模拟对象)。 在测试运行之前设置预期;如果预期的调用没有发生,测试就会失败。基于库的工具:Mockito、MockK、jMock。

日常交流中,大家会把这五种都称为“mock”。严谨讨论时,这些区别很重要:桩关注输入;模拟对象关注输出验证。

库(JVM):Mockito(Java 标准库)、MockK(符合 Kotlin 习惯,可处理 final 类和协程)、jMock(较老、严格)。Kotlin 项目选择 MockK;Java 项目可以使用 Mockito。

最大的风险:过度模拟。 模拟每个协作者的测试有两个问题:

  • 它们测试的是实现,而非行为。重构若改变了调用哪个协作者,即便外部可观察到的行为完全相同,测试也会失败。
  • 测试内容大多是模拟设置。读测试比读生产代码更费劲。

Beck 的“不要模拟自己不拥有的东西”原则。 模拟边界——你的代码与外部世界交互的接口。不要模拟外部世界本身。边界是你的,你可以一致地为它创建伪实现。外部世界是他们的;你的模拟会逐渐偏离真实行为,而你直到生产环境出问题才会察觉。

将模拟对象与契约测试配套使用。 如果你为单元测试伪造了 PaymentGateway,还要编写一个契约测试,分别针对伪实现和真实网关运行,并断言它们在重要用例中的行为一致。没有契约测试,伪实现就会逐渐偏离真实行为,甚至给出错误结果。

决策启发

  • 默认应保持谨慎。真实对象 > 伪实现 > 桩 > 间谍 > 模拟对象。选择实际可行且保真度最高的方案。
  • 在边界处模拟。在你的领域内部,优先使用真实对象。
  • 对于时间:注入 Clock。对于随机性:注入 Random。对于 HTTP:注入你自己拥有的小型接口,而不是直接注入第三方客户端。
  • 不要轻易验证“调用次数”。verify(mock, times(3)) 几乎总是在测试实现,而非行为。
  • 如果测试中设置模拟对象的行数多于断言的行数,就说明设计需要调整。

反模式

  • 模拟数据库。 很常见;但几乎总是错误的做法。使用内存实现(H2、搭配真实数据库的 Testcontainers,或伪仓库)。
  • 模拟值对象 / 数据类。 使用 data class Money(BigDecimal, Currency) 构造真实对象;不要 mock<Money>()。
  • 模拟框架。 测试验证是否调用了 Spring 的 @Transactional。你测试的是 Spring,而不是自己的代码。
  • 脆弱的调用次数验证。 无害的重构将仓库调用从两次改为一次,行为没有差异,却导致 verify(repo, times(2)) 失败。
  • 没有针对伪对象的契约测试。 伪对象的行为逐渐偏离真实对象;测试通过;生产环境出故障。
  • 模拟自己不拥有的对象(直接模拟第三方 SDK)。 将他们的 SDK 封装在由你拥有的小型接口后面,并模拟该接口。

另请参阅

  • tdd-test-isolation — 模拟是实现隔离的一种手段。
  • tdd-anti-patterns — 过度模拟和脆弱的验证是目录中列出的异味。
  • kp-testing-coroutines — TestDispatcher 是类似模拟对象的调度器替代品;同样的准则也适用。
  • gof-kotlin-idioms — Kotlin 的 MockK 支持 final 类和挂起函数。

参考资料

  • Kent Beck,Test-Driven Development By Example,第 27 章 — “Mock Object”条目及其 Self Shunt 变体。
  • Gerard Meszaros,xUnit Test Patterns — 对测试替身分类的详尽介绍。
  • Steve Freeman & Nat Pryce,Growing Object-Oriented Software, Guided by Tests — “模拟角色,而非对象”——正确实践的经典指导。

评论

支持 Markdown