Mockito 中 mock、spy、stub 和 verify 有什么区别?
简化版
mock 是纯模拟对象(假对象)——不含真实逻辑,未打桩的方法返回默认值(null/0/false)。spy 是包装真实对象的半假对象——未打桩的方法会调用真实逻辑,只替换你指定的少数方法。stub(打桩) 和 verify 是两类不同的动作:stub 是「规定依赖被调用时返回什么」(准备行为,对应测试的 Given);verify 是「验证某个交互是否发生」(检查行为,对应测试的 Then)。一句话:mock 是假对象、spy 是带真实行为的半假对象;stub 安排依赖怎么回应、verify 检查有意义的交互。
详细版
mock vs spy 对比:
| mock | spy | |
|---|---|---|
| 本质 | 纯假对象,无真实逻辑 | 包装真实对象 |
| 未打桩方法 | 返回默认值(null/0/false) | 调用真实方法 |
| 适用 | 外部依赖(DAO、远程客户端) | 保留大部分真实行为、只改少数方法 |
| 风险 | 低 | 高(可能触发真实副作用) |
stub(打桩)vs verify(验证):
// stub:安排依赖的返回(Given)
when(userDao.findById(1L)).thenReturn(new User("Alice"));
when(payGateway.pay(any())).thenThrow(new PayException());
// 被测方法执行(When)
orderService.createOrder(1L);
// verify:验证交互是否发生(Then)
verify(payGateway, times(1)).pay(any()); // 验证调用了一次支付
verify(emailService, never()).send(any()); // 验证没有发邮件
对 spy 打桩要用 doReturn 而非 when:
List<String> spyList = spy(new ArrayList<>());
// when(spyList.get(0)).thenReturn("x"); // ❌ 会先真的调用 get(0),空列表抛异常
doReturn("x").when(spyList).get(0); // ✅ 不会先调用真实方法
完整版教学
一、Mockito 要解决什么问题
单元测试关注一个类的业务逻辑,但这个类往往依赖外部协作者——数据库(DAO)、消息队列、HTTP 客户端、系统时间等。这些依赖在单元测试里不能或不想真实调用(慢、有副作用、不确定)。
Mockito 用「替身对象」切断这些外部依赖,让测试能:给定协作者的行为(stub)→ 执行被测方法 → 验证它的输出和交互(断言/verify)。这样测试只聚焦当前类的逻辑,快速、确定、可重复。
| 概念 | 属于什么 | 作用 |
|---|---|---|
mock | 替身对象 | 纯假对象,未打桩返回默认值 |
spy | 替身对象 | 半假对象,未打桩调用真实方法 |
stub | 测试动作 | 安排依赖返回值或异常 |
verify | 测试动作 | 验证有意义的交互是否发生 |
Mockito 题要先分清“对象”和“动作”:mock/spy 是对象,stub/verify 是你对对象做的测试安排和检查。
二、mock 的默认行为
mock 对象是纯假的,没有真实业务状态和逻辑。它的未打桩方法返回「默认值」:
- 返回对象的方法 → null。
- 返回数字的方法 → 0。
- 返回 boolean 的方法 → false。
- 返回集合的方法 → 空集合(Mockito 的默认 answer 会返回空集合而非 null,视配置)。
所以 mock 适合模拟被测对象的「边界依赖」(DAO、远程客户端)——你只需要它「在被这样调用时返回你安排的值」,不关心它内部怎么实现。mock 不适合替代被测对象本身(被测对象要用真实的,才有测试意义)。
三、spy 的风险(重点)
spy 包装一个真实对象——未打桩的方法会真的执行真实逻辑。这是它和 mock 最大的区别,也是风险所在:
- spy 的未打桩方法可能真的访问数据库、修改真实状态、触发昂贵或有副作用的逻辑。
- 用 spy 时如果忘了给某个方法打桩,它会默默执行真实实现,可能造成意外后果(改了真实数据、发了真实请求)。
使用 spy 前要确认真实方法安全可执行。而且——很多时候需要 spy 是「设计不好」的信号:如果你只想替换一个类的部分方法,往往是这个类职责太多、依赖没有拆开。把设计改成「依赖可注入」(把要替换的部分抽成独立依赖,用 mock 替换)通常比用 spy 局部篡改更清晰。spy 是「不得已」的工具,不是首选。
mock: method() -> 默认值,除非 stub
spy : method() -> 真实方法,除非 stub
四、stub 是「安排条件」(Given)
stub(打桩) 规定「当依赖被这样调用时,返回这个结果 / 抛这个异常」——它对应测试的 Given(前置条件) 阶段:
when(userDao.findById(1L)).thenReturn(user); // 安排返回值
when(dao.save(any())).thenThrow(new DbException()); // 安排抛异常
好的 stub 应该服务于业务场景——安排「这个用户存在」「这次支付会失败」这样有意义的条件,而不是为了让测试勉强跑过,把依赖内部的每一步都规定死(那样测试就和实现细节强绑定了)。
五、verify 是「观察行为」(Then)
verify 用来检查某个交互(方法调用)是否发生、发生了几次——它对应测试的 Then(验证) 阶段,适合验证副作用:
verify(emailService).send(any()); // 验证发了邮件
verify(payGateway, times(1)).pay(any()); // 验证调用支付恰好 1 次
verify(inventoryService, never()).reduce(any()); // 验证没有扣库存
verify 只应验证「有业务意义的交互」——是否发消息、是否调支付网关、是否扣库存。对「有返回值的纯逻辑」,直接断言返回值通常比 verify 内部调用更稳(见第七节)。
六、when 和 doReturn 的区别(spy 的坑)
打桩有两种写法,对 spy 时区别很关键:
when(spy.method()).thenReturn(x):这会先执行spy.method()(因为参数要先求值)——对 spy 而言,这会真的调用真实方法!如果真实方法有副作用或会抛异常(如对空列表get(0)),就出问题了。doReturn(x).when(spy).method():不会先调用真实方法,直接打桩。
所以对 spy、void 方法、以及会抛异常的方法打桩,要优先用 doReturn/doThrow/doNothing 风格,避免「打桩时意外触发真实逻辑」。这是 Mockito 高频坑。
七、可维护测试的边界(别过度 verify)
一个重要原则:测试应该验证「可观察的行为/结果」,而不是「复制实现细节」。
- 如果你重构了内部算法(换了实现方式)但业务输出不变,测试应该继续通过——因为业务行为没变。
- 过度 verify 的问题:如果你
verify了一堆内部私有流程、中间方法调用、调用顺序,那么一旦重构内部实现(哪怕结果一样),这些 verify 就全失败了——测试成了重构的阻力,而不是安全网。
所以:优先断言返回值和可观察的副作用,谨慎 verify 内部交互。verify 留给「真正重要的副作用」(发消息、扣款),不要验证每一步内部调用。
八、常见误区与追问
- 误区:mock 和 spy 都不会执行真实逻辑。 mock 默认不执行真实逻辑,spy 未打桩方法会调用真实实现。
- 误区:verify 越多测试越可靠。 过度 verify 内部细节会让重构变困难,应该验证业务结果和重要副作用。
- 误区:stub 是断言的一种。 stub 是 Given 阶段安排依赖行为,断言和 verify 才是 Then 阶段检查结果。
- 追问:为什么 spy 打桩常用
doReturn?when(spy.method())会先执行真实方法,可能抛异常或触发副作用。 - 追问:mock 适合替代什么对象? 适合替代外部依赖,如 DAO、远程客户端、消息发送器,不适合替代被测对象本身。
- 追问:什么时候 verify 比断言返回值更合适? 当要验证发消息、扣库存、调用支付网关等可观察副作用时更合适。
九、加强记忆
mock = 纯假对象(未打桩方法返回默认值 null/0/false,适合外部依赖如 DAO/远程客户端);spy = 包装真实对象的半假对象(未打桩方法调用真实逻辑,只改少数方法,有触发真实副作用的风险,需要 spy 常是设计信号——优先改成依赖可注入 + mock)。stub(打桩)= 安排依赖返回什么(when().thenReturn(),对应 Given);verify = 验证交互是否发生(verify(times/never),对应 Then,只验有业务意义的副作用)。对 spy 打桩用 doReturn().when() 而非 when().thenReturn()(后者会先真的调用方法,坑)。测试验「可观察行为/结果」而非复制实现——过度 verify 内部调用会让测试成为重构阻力,优先断言返回值。