
在单元测试中,我们经常使用 mockito 等框架来模拟依赖项,以便将测试范围限制在被测单元本身,实现测试的隔离性、可重复性和执行效率。然而,模拟对象(mock object)与真实对象有着本质的区别:
当你在单元测试中声明一个 @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:当模拟对象的某个方法以特定参数被调用时,应该返回什么。
考虑以下测试场景: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);
}
}在上述修正后的测试代码中:
通过正确理解和运用 Mockito 的模拟机制,我们可以编写出高效、可靠且易于维护的单元测试,确保业务逻辑的正确性。
以上就是理解与应用Mockito:为何模拟仓库无法保存数据及其解决方案的详细内容,更多请关注php中文网其它相关文章!
每个人都需要一台速度更快、更稳定的 PC。随着时间的推移,垃圾文件、旧注册表数据和不必要的后台进程会占用资源并降低性能。幸运的是,许多工具可以让 Windows 保持平稳运行。
Copyright 2014-2025 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号