
深入理解 Mockito 模拟对象
在单元测试中,我们经常使用 mockito 等框架来模拟依赖项,以便将测试范围限制在被测单元本身,实现测试的隔离性、可重复性和执行效率。然而,模拟对象(mock object)与真实对象有着本质的区别:
- 非实际执行者: 模拟对象并不会执行其真实对应类中的任何业务逻辑。例如,一个模拟的 UserRepository 不会连接数据库、执行 SQL 语句或实际存储数据。
- 行为定义: 模拟对象的行为需要被显式地定义。默认情况下,如果不对模拟对象的方法进行行为定义,它们会返回 Java 类型的默认值(例如,null 对于对象,0 对于 int,false 对于 boolean,空集合对于集合类型)。
- 状态无记忆: 模拟对象通常不具备状态记忆能力。即使你调用了模拟对象的 save() 方法,它也不会“记住”你传入的数据。因此,后续的 findAll() 或 findById() 调用将无法获取到之前“保存”的数据,除非你明确地定义了这些方法的返回行为。
模拟仓库无法保存数据的根源
当你在单元测试中声明一个 @Mock 的 UserRepository 并尝试调用 repository.save(appUser); 时,实际上并没有任何数据被持久化。这个 save() 调用只是在模拟对象上发生了一个方法调用,但它不会触发任何底层的数据存储机制。因此,当你随后尝试通过 userService.loadUserByUsername(appUser.getUsername()); 调用业务逻辑时,如果该业务逻辑内部依赖 repository.searchByUserName() 来获取用户,而你没有为这个模拟方法定义返回值,它将返回 Optional.empty(),从而导致 UsernameNotFoundException。
即使你在测试配置中包含了 H2 内存数据库的设置,这只是为 Spring Boot 应用程序提供了潜在的真实数据源配置。但在 AppUserServiceTest 中,由于 UserRepository 被 @Mock 注解,UserService 接收到的是一个模拟实例,而不是一个与 H2 数据库交互的真实 UserRepository 实例。
定义模拟行为:Mockito.when().thenReturn()
为了让模拟对象在特定方法被调用时返回预期的结果,我们需要使用 Mockito.when().thenReturn() 语法来定义其行为。这告诉 Mockito:当模拟对象的某个方法以特定参数被调用时,应该返回什么。
考虑以下测试场景:UserService 依赖 UserRepository 的 searchByUserName 方法来查找用户。为了测试 loadUserByUsername 方法的正确性,我们需要模拟 searchByUserName 的行为。
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.when;
import static org.mockito.Mockito.verify;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import java.util.Optional;
// 假设 AppUser 和 ApplicationRole 是你的实体和枚举类
// import your.package.AppUser;
// import your.package.ApplicationRole;
// import your.package.UserRepository;
// import your.package.UserService;
@ExtendWith(MockitoExtension.class)
@SpringBootTest // 注意:对于纯单元测试,通常不需要 @SpringBootTest,因为它会加载完整的应用上下文,增加测试时间。
// 如果仅测试 UserService 逻辑,移除此注解可以提高效率。
class AppUserServiceTest {
@Mock
private UserRepository repository; // 模拟 UserRepository
private UserService userService;
@BeforeEach
void setUp() {
// 注入模拟的 repository 到 userService
userService = new UserService(repository);
}
@Test
void itShouldLoadUsernameByName() {
// 1. 准备测试数据
AppUser appUser = new AppUser(
ApplicationRole.USER,
"leonardo",
"rossi",
"leo__",
"email@example.com", // 修正 email 格式
"password"
);
// 2. 定义模拟行为:当 repository.searchByUserName() 被调用时,返回包含 appUser 的 Optional
// 这里使用 any() 匹配器表示任何字符串参数都会触发此行为。
// 如果需要匹配特定参数,可以直接传入 appUser.getUsername()。
when(repository.searchByUserName(any(String.class))).thenReturn(Optional.of(appUser));
// 3. 执行被测方法
UserDetails foundUser = userService.loadUserByUsername(appUser.getUsername());
// 4. 验证结果
// 验证 repository.searchByUserName 方法是否被调用,且参数正确
verify(repository).searchByUserName(appUser.getUsername());
// 进一步断言返回的用户详情是否与预期一致
// 例如:
// assertThat(foundUser.getUsername()).isEqualTo(appUser.getUsername());
// assertThat(foundUser.getAuthorities()).containsExactlyInAnyOrderElementsOf(appUser.getAuthorities());
}
@Test
void itShouldThrowExceptionWhenUserNotFound() {
// 1. 准备数据
String nonExistentUsername = "nonexistent";
// 2. 定义模拟行为:当 repository.searchByUserName() 被调用时,返回空的 Optional
when(repository.searchByUserName(nonExistentUsername)).thenReturn(Optional.empty());
// 3. 验证异常是否抛出
org.junit.jupiter.api.Assertions.assertThrows(UsernameNotFoundException.class, () -> {
userService.loadUserByUsername(nonExistentUsername);
});
// 4. 验证 repository.searchByUserName 方法是否被调用
verify(repository).searchByUserName(nonExistentUsername);
}
}在上述修正后的测试代码中:
- when(repository.searchByUserName(any(String.class))).thenReturn(Optional.of(appUser)); 是核心。它告诉 Mockito,当 repository 对象的 searchByUserName 方法被调用,且传入的是任意 String 类型参数时,就返回一个包含 appUser 的 Optional 对象。
- any(String.class) 是 Mockito 提供的一个参数匹配器,用于匹配任何 String 类型的参数。如果你希望只在特定参数被调用时返回特定值,可以直接传入具体的参数,例如 when(repository.searchByUserName(appUser.getUsername())).thenReturn(Optional.of(appUser));。
- verify(repository).searchByUserName(appUser.getUsername()); 用于验证 repository 的 searchByUserName 方法是否确实被 userService 调用了,并且传入的参数是预期的 appUser.getUsername()。
注意事项与最佳实践
- 单元测试与集成测试的区别: 模拟对象主要用于单元测试,旨在隔离被测单元。如果你需要测试与数据库的实际交互(例如,确保 JPA 映射正确、事务行为符合预期),那么你需要进行集成测试,此时应使用真实的 UserRepository 实例(可能配合 @DataJpaTest)。
- 避免过度模拟: 并非所有依赖都需要模拟。对于简单的值对象或工具类,直接使用真实实例可能更简单。只有当依赖项是外部服务、数据库、复杂组件或耗时操作时,才考虑模拟。
- 清晰的模拟行为定义: 确保你的 when().thenReturn() 语句清晰地反映了业务逻辑在不同场景下的预期行为(例如,找到用户、找不到用户、抛出异常等)。
- 使用参数匹配器: any(), eq(), argThat() 等匹配器非常有用,但要谨慎使用 any(),因为它可能掩盖测试中参数传递的错误。在可能的情况下,尽量使用 eq() 或直接传入具体值。
- 静态导入: 为了代码简洁性,通常会静态导入 org.mockito.Mockito.when 和 org.mockito.ArgumentMatchers.any。
通过正确理解和运用 Mockito 的模拟机制,我们可以编写出高效、可靠且易于维护的单元测试,确保业务逻辑的正确性。










