深入Mockito测试案例:从零到实战的单元测试最佳实践(附详细代码解析)

目录导读
- 为什么Mockito是Java单元测试的“标配”?
- Mockito核心概念快速扫盲(Mock/Spy/InjectMocks)
- 实战案例一:基础Mock与验证(模拟UserService)
- 实战案例二:复杂场景——异常、回调与Answer接口
- 实战案例三:静态方法、构造器与Final类(使用Inline MockMaker)
- Mockito与JUnit5、Spring Boot的集成技巧
- 高频问题FAQ:为什么我的Mock不生效?
- 总结与避坑指南:5条铁律让测试更健壮
为什么Mockito是Java单元测试的“标配”?
在真实项目中,一个Service层方法往往依赖DAO、外部API、消息队列等,如果直接new真实对象,测试速度慢、不稳定(比如网络抖动),而且无法模拟极端异常,Mockito通过创建假对象(Mock),让你能精确控制依赖的行为,从而只测试当前类的逻辑,据统计,全球约70%的Java项目使用Mockito,因为它语法简洁、与JUnit无缝集成,且支持“行为驱动开发(BDD)”风格。
核心价值:Mockito把“依赖隔离”做到了极致,让单元测试真正“单元化”。
Mockito核心概念快速扫盲
- @Mock:创建mock对象,该对象的所有方法返回默认值(如null、0、false)。
- @Spy:保留真实对象的方法,但可以局部打桩(stub)覆盖个别方法。
- @InjectMocks:将mock或spy对象自动注入到被测类中(基于构造器、setter或字段注入)。
- when(...).thenReturn(...):打桩,定义“何时调用返回什么”。
- verify(...):验证某个方法是否被调用,以及调用次数、顺序。
小提示:使用
@ExtendWith(MockitoExtension.class)(JUnit5)或@RunWith(MockitoJUnitRunner.class)(JUnit4)来启用Mockito注解。
实战案例一:基础Mock与验证(模拟UserService)
假设有一个UserService,它依赖UserDao来查询用户。
public class UserService {
private final UserDao userDao;
public UserService(UserDao userDao) { this.userDao = userDao; }
public String getUsername(Integer id) {
User user = userDao.findById(id);
return (user == null) ? "unknown" : user.getName();
}
}
测试代码:
@ExtendWith(MockitoExtension.class)
class UserServiceTest {
@Mock UserDao userDao;
@InjectMocks UserService userService;
@Test
void testGetUsername_whenUserExists() {
User mockUser = new User(1, "Alice");
when(userDao.findById(1)).thenReturn(mockUser);
String name = userService.getUsername(1);
assertEquals("Alice", name);
verify(userDao).findById(1); // 验证确实调用了
}
@Test
void testGetUsername_whenUserNull() {
when(userDao.findById(2)).thenReturn(null);
assertEquals("unknown", userService.getUsername(2));
}
}
问答环节
问:
verify和when有什么区别?
答:when是打桩,调用mock方法时返回预设结果;verify是事后检查,确认某个方法是否被调用过,以及调用次数(times(2)、never()等)。
实战案例二:复杂场景——异常、回调与Answer接口
如果依赖方法需要抛出异常,或需要根据参数动态计算返回值,怎么办?
// 模拟异常
doThrow(new RuntimeException("DB down")).when(userDao).delete(anyInt());
// 使用Answer动态返回
when(userDao.findById(anyInt())).thenAnswer(invocation -> {
Integer id = invocation.getArgument(0);
return id > 100 ? new User(id, "BigID") : null;
});
回调场景:有些老代码使用回调接口,Mockito借助doAnswer可以模拟异步回调。
doAnswer(invocation -> {
Callback cb = invocation.getArgument(0);
cb.onSuccess("fakeResult");
return null;
}).when(mockClient).call(any(Callback.class));
问答环节
问:
when(...).thenReturn(...)和doReturn(...).when(...)有什么区别?
答:在方法有返回值时两者等价,但doReturn不执行真实方法,适合间谍对象(@Spy)或无返回值void方法(此时只能用doThrow/doAnswer),建议:对void方法一律用doXxx;对非void方法用when更直观。
实战案例三:静态方法、构造器与Final类(使用Inline MockMaker)
Mockito默认无法mock静态方法和final类,但通过配置mockito-inline依赖,可以开启实验性支持。
依赖配置(Maven):
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-inline</artifactId>
<version>5.2.0</version>
<scope>test</scope>
</dependency>
测试静态方法(例如UUID.randomUUID()):
try (MockedStatic<UUID> mocked = mockStatic(UUID.class)) {
mocked.when(UUID::randomUUID).thenReturn(new UUID(0, 0));
// 调用被测方法,验证逻辑
}
构造器mock:使用try (MockedConstruction<File> mocked = mockConstruction(File.class))。
警告:静态mock会污染其他测试,务必在
try-with-resources块内使用,确保自动关闭。
问答环节
问:mock静态方法后,测试方法内部创建了new对象,怎么mock构造器?
答:使用mockConstruction,在MockedConstruction.Context里获取构造参数并设置默认行为,但注意:过度使用静态mock往往暗示设计缺陷,优先考虑重构代码(如把静态方法封装进可注入类)。
Mockito与JUnit5、Spring Boot的集成技巧
在Spring Boot测试中,有两种常见用法:
- 纯单元测试:不用Spring上下文,直接
@Mock+@InjectMocks,跑得飞快。 - 整合测试(@WebMvcTest / @ServiceTest):配合
@MockBean(Spring包装的Mockito)。
@WebMvcTest(UserController.class)
class UserControllerTest {
@Autowired MockMvc mockMvc;
@MockBean UserService userService;
@Test
void testGetUser() throws Exception {
when(userService.getUsername(1)).thenReturn("Bob");
mockMvc.perform(get("/user/1"))
.andExpect(status().isOk())
.andExpect(content().string("Bob"));
}
}
关键点:@MockBean会把UserService的mock对象放进Spring容器,替换掉真实Bean,但重置状态在每个测试后自动完成。
高频问题FAQ:为什么我的Mock不生效?
-
情况1:Mock对象被
when打桩了,但实际调用的是真实对象。
原因:@InjectMocks注入失败(构造器参数匹配不上),导致被测类持有的是真实依赖。解决:检查注入方式,选择最明确的构造器注入并加上@Autowired。 -
情况2:
verify报错“Wanted but not invoked”。
原因:参数不匹配(例如anyInt()vseq(1)混用)。解决:统一使用eq()或纯anyInt(),禁止混合使用(除any()外)。 -
情况3:调用
when时报错“Unnecessary stubbing”。
原因:测试中打桩了但没有调用对应方法。解决:删除多余的stub,或者使用lenient()(Mockito 2.20+):lenient().when(...).thenReturn(...)。 -
情况4:mock静态方法报错“Mockito cannot mock this class”。
检查:是否引入mockito-inline,以及当前Mockito版本是否兼容。
总结与避坑指南:5条铁律让测试更健壮
- 铁律一:只mock外部依赖,Mockito用于隔离,不要对被测类自身的方法打桩,否则测试意义降低。
- 铁律二:精确控制参数,优先使用
eq()、anyInt()、contains()等匹配器,避免使用any()匹配所有导致误判。 - 铁律三:verify只验证必要的交互,过度验证会脆弱,别把每个方法都verify一遍。
- 铁律四:测试命名与行为对齐:建议使用
method_should_expectedBehavior_whenCondition格式,如getUsername_shouldReturnUnknown_whenUserNotFound。 - 铁律五:隔离静态mock:使用
try-with-resources,并考虑把静态方法抽到类里以提升可测性。
最终提醒:Mockito让测试代码更简单,但真正的高质量测试还需要结合“断言时机”、“覆盖率分析”和“代码重构”,建议持续重构代码以降低对过度mock的依赖——这会使测试不仅快,而且更能反映业务逻辑的正确性。