测试替身(Test Double)有哪几种?Dummy、Stub、Spy、Mock、Fake 怎么区分?
简化版
**「测试替身(Test Double)」是「在测试中替换真实依赖的各种假对象」的统称(就像电影里的替身演员)——当被测代码依赖别的组件(数据库、外部服务、其他类)时,用「替身」替换真实依赖,让测试隔离、可控、快速。**按 Gerard Meszaros 的经典分类,有五种:① Dummy(哑对象)——只是「占位」,传进去满足参数要求但不会被真正使用(如一个必须传但用不到的参数);② Stub(桩)——「预设固定的返回值」,让依赖返回你要的数据(when().thenReturn()),控制测试的输入;③ Spy(间谍)——「包装真实对象」,默认调用真实方法,但能「监视」(记录调用)或「部分替换」某些方法(真实+可打桩);④ Mock(模拟)——「预设期望 + 验证交互」,不仅能打桩,还能验证「方法被以什么参数、调用了几次」(verify());⑤ Fake(伪对象)——「有真实逻辑但简化的实现」(如用内存 Map 实现的假数据库、H2 内存库),能真正工作但不用于生产。关键区别:Stub 关注「返回什么」(状态验证)、Mock 关注「怎么被调用」(行为验证)、Spy 是「真实对象的部分替换」、Fake 是「简化的真实现」、Dummy 只占位。核心:测试替身是替换真实依赖的假对象,五种:Dummy(占位)、Stub(预设返回)、Spy(包装真实+监视)、Mock(打桩+验证交互)、Fake(简化实现)。
详细版
五种测试替身:
| 类型 | 特点 | 用途 |
|---|---|---|
| Dummy | 只占位,不被使用 | 满足参数,用不到 |
| Stub | 预设固定返回值 | 控制依赖返回(状态验证) |
| Spy | 包装真实对象,可监视/部分替换 | 真实+打桩部分方法 |
| Mock | 打桩 + 验证交互 | 验证方法被怎么调用(行为验证) |
| Fake | 简化的真实实现 | 能工作的替身(内存库) |
// Stub:预设返回值(控制依赖返回什么)
UserRepository stub = mock(UserRepository.class);
when(stub.findById(1L)).thenReturn(new User("Tom")); // 预设返回
// → 用 stub 让 findById 返回固定的用户(状态验证:验证结果)
// Mock:打桩 + 验证交互
EmailService mockEmail = mock(EmailService.class);
orderService.create(1L);
verify(mockEmail).send(anyString(), anyString()); // 验证交互(行为验证)
// Spy:包装真实对象,部分替换
List<String> spyList = spy(new ArrayList<>()); // 包装真实 ArrayList
spyList.add("a"); // 调用真实方法(真加进去)
when(spyList.size()).thenReturn(100); // 但 size 被打桩
verify(spyList).add("a"); // 还能验证调用
// Fake:简化的真实实现(如内存 Map 实现的 Repository)
class FakeUserRepository implements UserRepository {
private Map<Long, User> data = new HashMap<>(); // 用内存 Map 存
public User findById(Long id) { return data.get(id); } // 真实逻辑(简化)
public void save(User u) { data.put(u.getId(), u); }
}
// Dummy:只占位(满足参数,不会被用)
service.process(input, new DummyLogger()); // logger 传了但用不到
⚠️ 实践中「Mock」经常被当成所有测试替身的统称(尤其用 Mockito 时),但严格分类下五种有明确区别——面试时能分清「Stub(状态验证)vs Mock(行为验证)」是关键。Stub 和 Mock 的核心区别:Stub 关注「返回什么」(预设固定返回值,让依赖给被测代码喂数据,然后断言被测代码的结果——状态验证 State Verification);Mock 关注「怎么被调用」(预设期望 +
verify验证「方法被以什么参数、调用了几次」——行为验证 Behavior Verification)。举例:测「下单成功后发邮件」——如果你断言下单结果(订单状态对),依赖用 Stub(返回数据);如果你验证 emailService.send() 被调用了(发了邮件),依赖用 Mock(verify)。Spy 和 Mock 的区别:Mock 是「完全假的」(默认所有方法返回 null/默认值,你打桩什么它返回什么);Spy 是「包装真实对象」(默认调用真实方法,只打桩你想替换的部分)——Spy 用于「大部分用真实、小部分替换」的场景,但容易出坑(部分真实部分假、行为难预测)。Fake 是「能真正工作的简化实现」(内存库、H2),适合集成测试。Mockito 里mock()造 Mock/Stub、spy()造 Spy。
完整版教学
一、为什么需要测试替身
先理解测试替身的作用:
单元测试的目标:隔离地测"一个单元"(一个类/方法)
但被测代码常依赖别的组件:
- 数据库(Repository)
- 外部服务(HTTP 调用、消息队列)
- 其他类
依赖真实组件的问题:
① 慢——真连数据库、真发 HTTP(测试慢)
② 不可控——真实依赖返回什么不确定(数据变了测试就变)
③ 有副作用——真发邮件、真改数据库(测试有副作用)
④ 难覆盖异常——真实依赖难模拟"异常情况"(如网络失败)
⑤ 不隔离——依赖挂了测试就挂(测的不是被测单元)
测试替身的方案:
用"替身"替换真实依赖
→ 隔离(不依赖真实组件)、可控(替身返回你要的)、
快(不真连数据库)、无副作用、易模拟异常
替身(Test Double)的比喻:
像电影里的"替身演员"——替真演员做危险动作
测试里的替身——替真实依赖,让测试可控
所以测试替身 = 替换真实依赖的假对象(隔离、可控、快、无副作用)
测试替身的作用——单元测试目标是隔离地测一个单元,但被测代码常依赖别的组件(数据库/外部服务/其他类)。依赖真实组件的问题:慢、不可控(返回不确定)、有副作用(真发邮件改数据库)、难覆盖异常、不隔离(依赖挂测试就挂)。测试替身方案:用替身替换真实依赖(隔离、可控、快、无副作用、易模拟异常)。比喻电影替身演员。理解「测试替身:单元测试要隔离测一个单元、但依赖真实组件有问题(慢/不可控/有副作用/难覆盖异常/不隔离)、用替身替换(隔离可控快无副作用易模拟异常)」,就理解了测试替身的作用。
二、Dummy:占位
Dummy(哑对象)——只占位、不使用:
Dummy(哑对象):
只是"占位",传进去满足参数要求,但不会被真正使用
→ 方法签名要求传这个参数,但这次测试的路径用不到它
例:
一个方法 process(Input in, Logger logger)
但你测的路径不会用到 logger
→ 传一个 dummy logger(占位,不会被调用)
service.process(input, new DummyLogger()); // 或 mock(Logger.class)
特点:
① 只占位——满足参数要求
② 不被使用——测试路径不调用它(如果调用会报错/无所谓)
用途:
方法有必须传的参数,但当前测试用不到
→ 传 dummy 占位
实践:
常用 null(如果允许)或 mock(不打桩的 mock,默认行为)
→ mock 不打桩就是 dummy 般的存在(调用返回默认值)
所以 Dummy 只占位(满足参数、不被使用)
Dummy(哑对象) 只占位、不使用——传进去满足参数要求但不会被真正使用(方法签名要求传但测试路径用不到)。例:process(Input, Logger) 但测的路径不用 logger、传 dummy 占位。特点:只占位(满足参数)、不被使用。用途:方法有必须传的参数但当前测试用不到。实践:常用 null 或不打桩的 mock。理解「Dummy 哑对象只占位不使用(满足参数要求但测试路径用不到)、用于方法有必须传但用不到的参数、常用 null 或不打桩 mock」,就掌握了 Dummy。
三、Stub:预设返回值
Stub(桩)——预设固定返回值,控制依赖返回:
Stub(桩):
预设"固定的返回值",让依赖返回你要的数据
→ 控制"依赖返回什么"(给被测代码喂数据)
例:
when(userRepo.findById(1L)).thenReturn(new User("Tom"));
→ stub:findById(1) 固定返回这个 user
→ 被测代码调 findById 就拿到这个 user(可控)
用途:
控制依赖的返回,让被测代码在"确定的输入"下运行
→ 然后断言被测代码的结果(状态验证)
状态验证(State Verification):
用 stub 提供输入 → 执行被测代码 → 断言结果(值/状态)
例:
when(repo.findById(1)).thenReturn(user); // stub
Order order = service.createOrder(1); // 执行
assertEquals("PENDING", order.getStatus()); // 断言结果(状态验证)
→ 关注"结果对不对",不关注"怎么调用依赖"
Stub 的关注点:
"返回什么"——预设返回值,控制输入
→ 不验证交互(不 verify)
所以 Stub 预设固定返回值(控制依赖返回、状态验证:断言结果)
Stub(桩) 预设固定返回值、控制依赖返回——when(userRepo.findById(1L)).thenReturn(user)(findById 固定返回 user、被测代码调拿到、可控)。用途:控制依赖返回让被测代码在确定输入下运行、然后断言结果(状态验证)。状态验证(State Verification):用 stub 提供输入 → 执行 → 断言结果(值/状态,关注结果对不对、不关注怎么调用依赖、不 verify)。Stub 的关注点:返回什么(预设返回值控制输入)。理解「Stub 预设固定返回值控制依赖返回;用途:控制输入让被测代码在确定输入下运行、断言结果(状态验证 State Verification);关注返回什么不验证交互」,就掌握了 Stub。
四、Mock:验证交互
Mock(模拟)——打桩 + 验证交互:
Mock(模拟):
不仅能打桩(预设返回值),还能"验证交互"
→ 验证"方法被以什么参数、调用了几次"(verify)
例:
EmailService mockEmail = mock(EmailService.class);
orderService.create(1L); // 内部应该调 emailService.send
verify(mockEmail).send(anyString(), anyString()); // 验证 send 被调用
用途:
验证被测代码"正确地调用了依赖"(尤其无返回值的方法)
→ 行为验证(Behavior Verification)
行为验证(Behavior Verification):
执行被测代码 → 验证它"怎么调用依赖"(verify 交互)
例:
orderService.create(1); // 执行
verify(emailService).send(...); // 验证发了邮件(行为验证)
→ 关注"怎么调用依赖"(交互),不只是结果
Mock vs Stub(核心区别):
Stub:关注"返回什么"(预设返回值)→ 状态验证(断言结果)
Mock:关注"怎么被调用"(验证交互)→ 行为验证(verify)
→ Stub 喂数据、Mock 验证交互
实践中:
Mockito 的 mock() 造的对象,既能当 Stub(when.thenReturn)
又能当 Mock(verify)→ 一个对象两用
→ "Mock"常被当所有替身的统称(但严格分类有区别)
所以 Mock 打桩+验证交互(行为验证:verify 方法被怎么调用)
Mock(模拟) 打桩 + 验证交互——不仅打桩还能验证交互(verify 验证方法被以什么参数、调用了几次)。用途:验证被测代码正确调用了依赖(尤其无返回值方法)、行为验证(Behavior Verification)。行为验证:执行 → 验证怎么调用依赖(关注交互不只结果)。Mock vs Stub(核心区别):Stub 关注返回什么(预设返回值)→ 状态验证(断言结果)、Mock 关注怎么被调用(验证交互)→ 行为验证(verify)。实践中 Mockito 的 mock() 既能当 Stub 又能当 Mock(一个对象两用、Mock 常被当统称)。理解「Mock 打桩+验证交互(verify 方法被怎么调用)、行为验证;Mock vs Stub 核心区别:Stub 关注返回什么(状态验证断言结果)、Mock 关注怎么被调用(行为验证 verify);Mockito mock 一个对象两用」,就掌握了 Mock。
五、Spy 与 Fake
Spy(间谍)和 Fake(伪对象)——理解这两种:
Spy(间谍):包装真实对象,默认调真实方法,可监视/部分替换
List<String> spy = spy(new ArrayList<>()); // 包装真实 ArrayList
spy.add("a"); // ★ 调用真实方法(真的加进去了)
when(spy.size()).thenReturn(100); // 但 size() 被打桩(部分替换)
verify(spy).add("a"); // 还能验证调用(监视)
特点:
① 默认调用真实对象的方法(不像 Mock 默认返回 null)
② 可以"部分打桩"(替换某些方法,其余用真实)
③ 可以监视(记录调用、verify)
用途:
大部分用真实逻辑、只替换/监视个别方法
坑:
部分真实部分假 → 行为难预测、容易出错
→ 谨慎用(尽量用 Mock,行为更清晰)
Fake(伪对象):有真实逻辑但简化的实现(能工作的替身)
class FakeUserRepository implements UserRepository {
Map<Long, User> data = new HashMap<>(); // 用内存 Map
public User findById(Long id) { return data.get(id); } // 真实逻辑(简化)
public void save(User u) { data.put(u.getId(), u); }
}
例:
- 内存数据库(H2)替真数据库
- 内存 Map 实现的 Repository
- 简化的假实现(能工作但不用于生产)
特点:
① 有真实的逻辑(能真正工作)
② 简化(如用内存代替真数据库)
用途:
需要"能工作的替身"(如集成测试用内存库)
→ 比 Mock/Stub 更真实(有逻辑),但不用于生产
Spy vs Mock:Mock 完全假、Spy 包装真实(部分替换)
Fake vs Stub:Stub 预设返回值、Fake 有真实逻辑(能工作)
所以 Spy 包装真实(部分替换/监视)、Fake 简化的真实现(能工作,如内存库)
Spy(间谍) 包装真实对象、默认调真实方法、可监视/部分替换——spy(new ArrayList<>())(add 调真实方法真加进去、size() 打桩部分替换、verify 监视)。特点:默认调真实方法(不像 Mock 返回 null)、可部分打桩、可监视。用途:大部分真实只替换个别方法。坑:部分真实部分假、行为难预测易出错(谨慎用)。Fake(伪对象) 有真实逻辑但简化的实现(能工作)——内存 Map 实现的 Repository、H2 内存库。特点:有真实逻辑(能工作)、简化(内存代替真数据库)。用途:需要能工作的替身(集成测试内存库)。Spy vs Mock:Mock 完全假、Spy 包装真实。Fake vs Stub:Stub 预设返回值、Fake 有真实逻辑。理解「Spy 包装真实对象(默认调真实方法+部分打桩+监视,坑是部分真实部分假难预测)、Fake 简化的真实现(有真实逻辑能工作,如内存库,用于集成测试);Spy vs Mock:Mock 完全假 Spy 包装真实;Fake vs Stub:Stub 预设返回 Fake 有逻辑」,就掌握了 Spy 和 Fake。
六、区分与实践
总结五种替身的区分和实践:
五种替身的核心区分:
Dummy:只占位、不使用(满足参数)
Stub:预设返回值(控制输入、状态验证)
Mock:打桩 + 验证交互(行为验证 verify)
Spy:包装真实对象、部分替换/监视
Fake:简化的真实实现(能工作、内存库)
按"关注点"区分:
Dummy:不关注(占位)
Stub:关注"返回什么"(喂数据)
Mock:关注"怎么被调用"(验证交互)
Spy:真实 + 部分替换
Fake:简化的真实现
实践建议:
① 控制依赖返回、断言结果 → Stub(when.thenReturn + assert)
② 验证依赖被正确调用(无返回值)→ Mock(verify)
③ 需要能工作的替身(集成测试)→ Fake(内存库)
④ 大部分真实只替换个别 → Spy(谨慎,容易出坑)
⑤ 占位用不到的参数 → Dummy(null / 不打桩 mock)
实践中的模糊:
Mockito 的 mock() 既能当 Stub 又能当 Mock
→ "Mock"常被当所有替身的统称(但面试要能区分)
→ 重点区分:Stub(状态验证)vs Mock(行为验证)
核心总结:
测试替身 = 替换真实依赖的假对象(隔离、可控、快)
五种:Dummy(占位)/Stub(预设返回,状态验证)/Mock(验证交互,行为验证)
/Spy(包装真实,部分替换)/Fake(简化实现,能工作)
关键:Stub 关注返回什么、Mock 关注怎么被调用
五种替身的核心区分:Dummy(占位)、Stub(预设返回,状态验证)、Mock(验证交互,行为验证)、Spy(包装真实,部分替换)、Fake(简化实现,能工作)。按关注点:Stub 关注返回什么、Mock 关注怎么被调用。实践:控制返回断言结果用 Stub、验证调用用 Mock、能工作的替身用 Fake、大部分真实用 Spy(谨慎)、占位用 Dummy。实践模糊:Mockito mock() 两用、Mock 常被当统称(面试要能区分 Stub vs Mock)。理解「五种区分:Dummy 占位/Stub 预设返回状态验证/Mock 验证交互行为验证/Spy 包装真实/Fake 简化实现;实践:返回断言用 Stub、验证调用用 Mock、能工作用 Fake、大部分真实用 Spy、占位用 Dummy;重点区分 Stub(状态)vs Mock(行为)」,就掌握了区分与实践。
记忆钩子:「测试替身(Test Double)=替换真实依赖的假对象(隔离/可控/快/无副作用,像替身演员);五种(Gerard Meszaros 分类):①Dummy(哑对象,只占位不使用,满足参数但测试路径用不到)②Stub(桩,预设固定返回值 when.thenReturn,控制依赖返回,★状态验证:断言被测代码结果,关注返回什么)③Mock(模拟,打桩+验证交互 verify,★行为验证:验证方法被以什么参数调了几次,关注怎么被调用)④Spy(间谍,包装真实对象默认调真实方法+部分打桩替换+监视,坑是部分真实部分假难预测)⑤Fake(伪对象,有真实逻辑但简化的实现,能工作,如内存 Map 实现的 Repository/H2 内存库,用于集成测试);★核心区别:Stub 关注返回什么(状态验证)vs Mock 关注怎么被调用(行为验证);Mockito mock()一个对象既当 Stub 又当 Mock、常被当统称」。
七、常见误区与追问
- 误区:所有测试替身都叫 Mock。 严格分类下有五种(Dummy、Stub、Spy、Mock、Fake),各有区别;只是实践中(尤其用 Mockito 时)「Mock」经常被当成所有测试替身的统称;面试时要能区分,尤其 Stub(状态验证、关注返回什么)和 Mock(行为验证、关注怎么被调用)。
- 误区:Stub 和 Mock 是一回事。 核心区别在关注点:Stub 关注「返回什么」(预设固定返回值,让依赖给被测代码喂数据,然后断言被测代码的结果——状态验证);Mock 关注「怎么被调用」(预设期望 + verify 验证方法被以什么参数、调用了几次——行为验证);Stub 喂数据、Mock 验证交互。
- 误区:Spy 和 Mock 差不多。 不同——Mock 是「完全假的」(默认所有方法返回 null/默认值,你打桩什么它返回什么);Spy 是「包装真实对象」(默认调用真实方法,只打桩你想替换的部分);Spy 用于「大部分用真实、小部分替换」,但容易出坑(部分真实部分假、行为难预测),一般优先用 Mock(行为更清晰)。
- 误区:Fake 就是 Stub。 不同——Stub 是预设固定返回值(when().thenReturn(),没有真实逻辑);Fake 是「有真实逻辑但简化的实现」(如用内存 Map 实现的 Repository、H2 内存数据库,能真正工作、有逻辑),比 Stub 更真实,适合集成测试用能工作的替身。
- 追问:测试替身有哪几种,怎么区分? 五种(Gerard Meszaros 分类):① Dummy(哑对象)——只占位、传进去满足参数要求但不会被真正使用;② Stub(桩)——预设固定的返回值,控制依赖返回什么(状态验证,关注返回什么);③ Mock(模拟)——打桩 + 验证交互(行为验证,验证方法被以什么参数、调用了几次);④ Spy(间谍)——包装真实对象,默认调用真实方法,可部分替换/监视;⑤ Fake(伪对象)——有真实逻辑但简化的实现(如内存库),能真正工作;核心区分:Stub 关注「返回什么」(状态验证)、Mock 关注「怎么被调用」(行为验证)。
- 追问:什么是状态验证和行为验证? 状态验证(State Verification):用 Stub 给被测代码提供输入(预设依赖的返回值),执行被测代码后断言它的结果(值/状态是否正确),关注「结果对不对」;行为验证(Behavior Verification):执行被测代码后,用 verify 验证它「怎么调用了依赖」(方法被以什么参数、调用了几次),关注「交互是否正确」;Stub 对应状态验证、Mock 对应行为验证;有返回值的方法适合状态验证、无返回值的方法(save/send)只能用行为验证。
- 追问:Fake 和 Mock/Stub 有什么区别,什么时候用 Fake? Mock/Stub 没有真实逻辑(Stub 预设固定返回值、Mock 验证交互);Fake 有真实但简化的逻辑(如内存 Map 实现的 Repository、H2 内存数据库),能真正工作(真的存取数据、真的执行逻辑,只是简化、不用于生产);用 Fake 的场景:需要一个「能真正工作的替身」,比如集成测试用 H2 内存数据库替代真实数据库(真的建表、存取),或用内存实现的假服务;比 Mock/Stub 更真实(有逻辑、能处理各种输入),但比真实依赖轻量快速。
八、加强记忆
「测试替身(Test Double)」是「在测试中替换真实依赖的各种假对象」的统称(像替身演员)——被测代码依赖别的组件(数据库、外部服务)时,用替身替换让测试隔离、可控、快、无副作用。五种(Gerard Meszaros 分类):① Dummy(哑对象)——只占位、传进去满足参数但不被真正使用;② Stub(桩)——预设固定返回值(when().thenReturn()),控制依赖返回什么(状态验证:断言被测代码结果,关注返回什么);③ Mock(模拟)——打桩 + 验证交互(verify,行为验证:验证方法被以什么参数、调用了几次,关注怎么被调用);④ Spy(间谍)——包装真实对象(默认调真实方法,可部分打桩替换/监视,坑是部分真实部分假、行为难预测,谨慎用);⑤ Fake(伪对象)——有真实逻辑但简化的实现(能工作,如内存 Map 实现的 Repository、H2 内存库,用于集成测试)。核心区别:Stub 关注「返回什么」(状态验证)vs Mock 关注「怎么被调用」(行为验证);Spy 是真实对象的部分替换、Fake 是简化的真实现。实践中 Mockito 的 mock() 既能当 Stub 又能当 Mock、「Mock」常被当所有替身的统称(面试要能区分)。一句话「测试替身=替换真实依赖的假对象(隔离可控快);五种:Dummy(占位)/Stub(预设返回,状态验证关注返回什么)/Mock(验证交互,行为验证关注怎么被调用)/Spy(包装真实,部分替换)/Fake(简化实现能工作,内存库);核心区别 Stub 关注返回什么 vs Mock 关注怎么被调用;Mockito mock 常被当统称」。