Mockito 原理与实战
一、Mockito 核心原理
1. 动态代理与字节码生成
Mockito 并非使用标准的 Java 动态代理(java.lang.reflect.Proxy
),而是基于 CGLIB 和 ASM 库,在运行时:
- 动态生成目标类的子类(对于非 final 类)
- 或接口的实现类(对于接口)
- 重写所有方法,替换为“可控制”的行为
✅ 优势:可以 mock 普通类、抽象类、接口,甚至部分方法(
spy
)
2. Mock 的创建过程
UserRepository mockRepo = mock(UserRepository.class);
执行过程:
- 生成子类字节码:CGLIB 创建
UserRepository$$EnhancerByMockito
类 - 方法拦截:所有方法调用被重定向到
MockHandler
- 行为匹配:根据
when(...).thenReturn(...)
的 stubbing 规则返回值或抛异常 - 记录调用:用于后续
verify(...)
验证
3. Stubbing(打桩) vs Verification(验证)
阶段 | 目的 | 使用方法 |
---|---|---|
Stubbing | 预设方法返回值 | when(mock.method()).thenReturn(value) |
Verification | 验证方法是否被调用 | verify(mock).method() |
4. Mock vs Spy
类型 | 行为 | 适用场景 |
---|---|---|
mock(Class) | 全新虚拟对象,所有方法默认返回 null /0 /false | 完全隔离依赖 |
spy(Object) | 真实对象,但可部分 mock 方法 | 测试部分逻辑,保留其他真实行为 |
二、Mockito 核心用法详解
示例:UserServiceTest.java
// 引入 JUnit 5 的测试类
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
// 引入 Mockito 注解和核心类
import org.mockito.*;
// 引入断言库(AssertJ)
import static org.assertj.core.api.Assertions.*;
// 引入 Mockito 的 BDD 风格语法(given/when/then)
import static org.mockito.BDDMockito.*;// 使用 JUnit 5 的扩展,自动处理 Mockito 注解
class UserServiceTest {// @Mock:创建一个 UserRepository 的 mock 对象// 所有方法默认返回 null(对于对象)、0(int)、false(boolean)等@Mockprivate UserMapper userMapper;// @InjectMocks:创建 UserService 实例,并自动将上面的 @Mock 注入到其字段中// 相当于:userService = new UserService(userMapper);@InjectMocksprivate UserService userService;// @BeforeEach:在每个测试方法执行前运行@BeforeEachvoid setUp() {// 初始化所有 @Mock 和 @InjectMocks 注解// 必须调用,否则 mock 不会生效MockitoAnnotations.openMocks(this);}// 测试:创建用户是否成功@Testvoid shouldCreateUserSuccessfully() {// Given:准备测试数据和预设行为(Arrange)// 创建一个待插入的用户对象User user = new User("Alice", "alice@example.com");// 设置 ID 为 null,表示尚未保存user.setId(1L);// 打桩(Stubbing):当调用 userMapper.insert(any(User.class)) 时// any(User.class) 表示匹配任意 User 类型的参数// thenAnswer 允许我们编写更复杂的逻辑,比如模拟主键回填when(userMapper.insert(any(User.class))).thenAnswer(invocation -> {// 获取传入的参数(即要插入的 user 对象)User u = invocation.getArgument(0);// 模拟数据库生成主键的行为,设置 ID = 1u.setId(1L);// 返回 1 表示插入成功(影响行数)return 1;});// When:执行被测方法(Act)// 调用 userService 的 createUser 方法User result = userService.createUser("Alice", "alice@example.com");// Then:验证结果(Assert)// 断言:返回的用户 ID 应该是 1assertThat(result.getId()).isEqualTo(1L);// 验证:userMapper.insert 方法是否被调用了一次// argThat(...) 用于自定义参数匹配器// 这里验证传入的 User 对象的 name 字段是 "Alice"verify(userMapper).insert(argThat(u -> u.getName().equals("Alice")));}// 测试:当用户不存在时应抛出异常@Testvoid shouldThrowExceptionWhenUserNotFound() {// Given:预设行为// 当调用 findById(999L) 时返回 null(表示数据库查不到)given(userMapper.findById(999L)).willReturn(null);// When & Then:执行并验证异常(AssertJ 风格)// 断言:调用 getUserById(999L) 会抛出 UserNotFoundExceptionassertThatThrownBy(() -> userService.getUserById(999L))// 并且异常类型是 UserNotFoundException.isInstanceOf(UserNotFoundException.class)// 并且异常消息包含 "999".hasMessageContaining("999");}// 测试:获取所有用户@Testvoid shouldReturnAllUsers() {// Given:预设 findAll() 返回一个包含一个用户的列表User user1 = new User("Bob", "bob@example.com");user1.setId(2L);// 将预设行为绑定到 mock 对象given(userMapper.findAll()).willReturn(java.util.Arrays.asList(user1));// When:调用服务方法List<User> result = userService.getAllUsers();// Then:验证结果assertThat(result).hasSize(1); // 列表大小为 1assertThat(result.get(0).getName()).isEqualTo("Bob"); // 第一个用户名字是 Bob}// 测试:更新用户@Testvoid shouldUpdateUserSuccessfully() {// Given:预设 findById 返回一个用户,update 返回影响行数 1User existingUser = new User("OldName", "old@example.com");existingUser.setId(1L);given(userMapper.findById(1L)).willReturn(existingUser);given(userMapper.update(any(User.class))).willReturn(1);// When:调用更新方法User updated = userService.updateUser(1L, "NewName", "new@example.com");// Then:验证assertThat(updated.getName()).isEqualTo("NewName");// 验证 update 方法被调用了一次verify(userMapper).update(argThat(u ->u.getName().equals("NewName") &&u.getEmail().equals("new@example.com")));}// 测试:删除用户@Testvoid shouldDeleteUser() {// Given:预设 findById 返回用户,deleteById 返回 1given(userMapper.findById(1L)).willReturn(new User("ToDelete", "del@example.com"));given(userMapper.deleteById(1L)).willReturn(1);// When:执行删除userService.deleteUser(1L);// Then:验证 deleteById 被调用了一次,参数是 1Lverify(userMapper).deleteById(eq(1L)); // eq(1L) 明确匹配 1L}// 测试:验证方法调用次数@Testvoid shouldVerifyCallCount() {// Givengiven(userMapper.findAll()).willReturn(java.util.Collections.emptyList());// WhenuserService.getAllUsers();userService.getAllUsers(); // 调用两次// Then:验证 findAll() 被调用了 2 次verify(userMapper, times(2)).findAll();// 其他调用次数验证:// verify(userMapper, never()).deleteById(999L); // 从未调用// verify(userMapper, atLeastOnce()).findAll(); // 至少调用一次// verify(userMapper, atMost(3)).findAll(); // 最多调用 3 次}// 测试:抛出异常@Testvoid shouldThrowExceptionOnSave() {// Given:当 insert 被调用时抛出 RuntimeExceptiondoThrow(new RuntimeException("DB Error")).when(userMapper).insert(any(User.class));// When & ThenassertThatThrownBy(() -> userService.createUser("Fail", "fail@example.com")).hasMessageContaining("DB Error");}// 测试:Spy(部分模拟)@Testvoid shouldUseSpy() {// 创建一个真实 ArrayList,并用 spy 包装List<String> list = spy(new java.util.ArrayList<String>());// 预设 get(0) 返回 "mocked"when(list.get(0)).thenReturn("mocked");// add() 仍执行真实逻辑list.add("real");// 验证assertThat(list.get(0)).isEqualTo("mocked"); // 被 mockassertThat(list.get(1)).isEqualTo("real"); // 真实行为assertThat(list).hasSize(2); // 真实 size}
}
三、高级用法与最佳实践
1. 参数匹配器(Argument Matchers)
匹配器 | 说明 |
---|---|
any() | 任意对象 |
anyString() | 任意字符串 |
eq("value") | 精确匹配 |
argThat(x -> x > 5) | 自定义条件 |
isNull() | null 值 |
same(obj) | 同一个引用 |
⚠️ 注意:不能混用具体值和匹配器
❌when(repo.findById(1L, anyString()))
→ 错误
✅when(repo.findById(eq(1L), anyString()))
→ 正确
2. BDD 风格写法(推荐)
// Given
given(userMapper.findById(1L)).willReturn(user);// When
User result = userService.getUserById(1L);// Then
then(userMapper).should().findById(1L);
3. Mockito 扩展(JUnit 5)
@ExtendWith(MockitoExtension.class)
class UserServiceTest {@Mock UserMapper mapper;@InjectMocks UserService service;// 不需要 @BeforeEach 中的 openMocks()
}
四、常见问题与陷阱
问题 | 解决方案 |
---|---|
NullPointerException | 忘记 MockitoAnnotations.openMocks(this); |
UnnecessaryStubbing | 存在未使用的 when(...) |
Wanted but not invoked | verify 的方法未被调用 |
Mockito cannot mock this class | 类是 final / 无参构造函数私有 |