一、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)等
@Mock
private UserMapper userMapper;
// @InjectMocks:创建 UserService 实例,并自动将上面的 @Mock 注入到其字段中
// 相当于:userService = new UserService(userMapper);
@InjectMocks
private UserService userService;
// @BeforeEach:在每个测试方法执行前运行
@BeforeEach
void setUp() {
// 初始化所有 @Mock 和 @InjectMocks 注解
// 必须调用,否则 mock 不会生效
MockitoAnnotations.openMocks(this);
}
// 测试:创建用户是否成功
@Test
void 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 = 1
u.setId(1L);
// 返回 1 表示插入成功(影响行数)
return 1;
});
// When:执行被测方法(Act)
// 调用 userService 的 createUser 方法
User result = userService.createUser("Alice", "alice@example.com");
// Then:验证结果(Assert)
// 断言:返回的用户 ID 应该是 1
assertThat(result.getId()).isEqualTo(1L);
// 验证:userMapper.insert 方法是否被调用了一次
// argThat(...) 用于自定义参数匹配器
// 这里验证传入的 User 对象的 name 字段是 "Alice"
verify(userMapper).insert(argThat(u -> u.getName().equals("Alice")));
}
// 测试:当用户不存在时应抛出异常
@Test
void shouldThrowExceptionWhenUserNotFound() {
// Given:预设行为
// 当调用 findById(999L) 时返回 null(表示数据库查不到)
given(userMapper.findById(999L)).willReturn(null);
// When & Then:执行并验证异常(AssertJ 风格)
// 断言:调用 getUserById(999L) 会抛出 UserNotFoundException
assertThatThrownBy(() -> userService.getUserById(999L))
// 并且异常类型是 UserNotFoundException
.isInstanceOf(UserNotFoundException.class)
// 并且异常消息包含 "999"
.hasMessageContaining("999");
}
// 测试:获取所有用户
@Test
void 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); // 列表大小为 1
assertThat(result.get(0).getName()).isEqualTo("Bob"); // 第一个用户名字是 Bob
}
// 测试:更新用户
@Test
void shouldUpdateUserSuccessfully() {
// Given:预设 findById 返回一个用户,update 返回影响行数 1
User 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")
));
}
// 测试:删除用户
@Test
void shouldDeleteUser() {
// Given:预设 findById 返回用户,deleteById 返回 1
given(userMapper.findById(1L)).willReturn(new User("ToDelete", "del@example.com"));
given(userMapper.deleteById(1L)).willReturn(1);
// When:执行删除
userService.deleteUser(1L);
// Then:验证 deleteById 被调用了一次,参数是 1L
verify(userMapper).deleteById(eq(1L)); // eq(1L) 明确匹配 1L
}
// 测试:验证方法调用次数
@Test
void shouldVerifyCallCount() {
// Given
given(userMapper.findAll()).willReturn(java.util.Collections.emptyList());
// When
userService.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 次
}
// 测试:抛出异常
@Test
void shouldThrowExceptionOnSave() {
// Given:当 insert 被调用时抛出 RuntimeException
doThrow(new RuntimeException("DB Error"))
.when(userMapper).insert(any(User.class));
// When & Then
assertThatThrownBy(() -> userService.createUser("Fail", "fail@example.com"))
.hasMessageContaining("DB Error");
}
// 测试:Spy(部分模拟)
@Test
void 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"); // 被 mock
assertThat(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 / 无参构造函数私有 |