
本文旨在解决 Mockito 中 `repository.save()` 方法返回 `null` 值的问题,该问题通常由桩设(stubbing)时参数匹配不准确引起,特别是当实体类(如 `User`)的 `equals/hashCode` 方法依赖于数据库自动生成的 ID 字段时。文章将深入探讨其根本原因,并提供两种有效的解决方案:转换为集成测试以模拟真实持久化行为,或在单元测试中使用 Mockito 的 `any()` 匹配器结合 `thenAnswer` 来动态模拟 ID 生成,从而确保测试的准确性和健壮性。
在进行单元测试时,我们经常使用 Mockito 来模拟依赖项的行为,例如 UserRepository。当遇到 repository.save() 方法返回 null 值并伴随 Strict stubbing argument mismatch 错误时,其核心原因通常在于 Mockito 在桩设(stubbing)时对参数的匹配规则与实际调用时传入的参数不符。
具体到 repository.save(inputUser) 的场景,问题出在以下两点:
User inputUser = User.builder()
// ...
.userID(1) // 此处设置了userID
.build();
Mockito.when(repository.save(inputUser)).thenReturn(outputUser);然而,在 UserServiceImpl 的 saveUser 方法中,User 对象是在调用 repository.save 之前构建的,此时 userID 通常是未设置的(默认为 0 或 null,因为它是数据库自动生成的):
public User saveUser(UserModel userModel) {
// ...
User user = User.builder().
// ...
.build(); // userID在此处未设置,默认为0
User returnedUser = userRepository.save(user); // 实际传入的user的userID是0
// ...
}因此,当 UserServiceImpl 调用 repository.save(user) 时,传入的 user 对象的 userID 为 0,而您在 setUp 中桩设的 inputUser 对象的 userID 为 1。由于 User 类的 equals 方法考虑了 userID,这两个对象被 Mockito 判定为不相等,导致桩设的 when 条件不满足,save 方法最终返回 null,并抛出 Strict stubbing argument mismatch 异常。
对于涉及持久化层(如数据库 ID 自动生成)的业务逻辑测试,通常推荐使用集成测试而非纯粹的单元测试。集成测试能够更真实地模拟应用程序在生产环境中的行为,包括数据库的交互和 ID 的生成机制。
优点:
实现方式: 将您的测试类转换为 @SpringBootTest,并使用真实的数据库(可以是内存数据库如 H2)进行测试。这样,UserRepository.save() 方法将实际与数据库交互,并由数据库负责生成 userID。
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.ActiveProfiles;
import static org.assertj.core.api.Assertions.assertThat;
@SpringBootTest // 启用Spring Boot测试环境
@ActiveProfiles("test") // 可以指定一个测试配置文件
public class UserServiceIntegrationTest {
@Autowired
private UserServiceImpl userServiceImpl; // 注入真实的服务实现
@Autowired
private UserRepository userRepository; // 注入真实的Repository,用于清理数据等
@Test
void whenSaveUser_ThenUserHasID() {
// Arrange
UserModel inputUserModel = new UserModel();
inputUserModel.setEmail("test@example.com");
inputUserModel.setFirstName("john");
inputUserModel.setLastName("doe");
inputUserModel.setPassword("test");
inputUserModel.setMatchPassword("test");
// Act
User savedUser = userServiceImpl.saveUser(inputUserModel);
// Assert
assertThat(savedUser).isNotNull();
assertThat(savedUser.getUserID()).isNotNull(); // 验证ID已被生成
assertThat(savedUser.getUserID()).isPositive(); // 验证ID是正数
// 可以进一步从数据库中查询验证
userRepository.findById(savedUser.getUserID()).ifPresent(
foundUser -> assertThat(foundUser.getEmail()).isEqualTo(inputUserModel.getEmail())
);
}
}注意事项:
如果您坚持使用单元测试并模拟 UserRepository,则需要更灵活地处理 save 方法的参数匹配和返回值。这可以通过结合使用 Mockito 的 any() 匹配器和 thenAnswer 回调来实现。
核心思路:
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.test.util.ReflectionTestUtils; // 用于设置私有字段
import static org.mockito.ArgumentMatchers.any; // 引入any()
import static org.mockito.Mockito.when;
import static org.assertj.core.api.Assertions.assertThat; // 推荐使用AssertJ
@ExtendWith(MockitoExtension.class)
public class UserServiceTest {
@Mock
private UserRepository repository; // 模拟UserRepository
@InjectMocks
private UserServiceImpl userServiceImpl; // 注入UserServiceImpl,其依赖会通过Mock自动注入
// 不再需要在setUp中桩设,直接在测试方法中桩设更清晰
@Test
void saveUser_userHasID() {
// Arrange
final UserModel inputUserModel = new UserModel();
inputUserModel.setEmail("test@example.com");
inputUserModel.setFirstName("john");
inputUserModel.setLastName("doe");
inputUserModel.setPassword("test");
inputUserModel.setMatchPassword("test");
// 桩设repository.save方法:
// 1. 使用any(User.class)匹配任何User对象
// 2. 使用thenAnswer模拟ID生成:获取传入的User对象,并使用ReflectionTestUtils设置其userID
when(repository.save(any(User.class))).thenAnswer(invocation -> {
final User entity = invocation.getArgument(0); // 获取传入save方法的User对象
// 模拟数据库生成ID的行为,这里使用一个随机长整型ID
// ReflectionTestUtils用于设置私有字段,如果userID有setter方法则可以直接调用
ReflectionTestUtils.setField(entity, "userID", Math.abs(new java.util.Random().nextLong()));
return entity; // 返回带有生成ID的User对象
});
// Act
final User user = userServiceImpl.saveUser(inputUserModel);
// Assert
assertThat(user).isNotNull();
assertThat(user.getUserID()).isNotNull(); // 验证ID已被生成
assertThat(user.getUserID()).isPositive(); // 验证ID是正数
assertThat(user.getEmail()).isEqualTo(inputUserModel.getEmail()); // 验证其他字段是否正确
}
}代码说明:
解决 Mockito save 方法返回 null 的问题,关键在于理解 Mockito 的参数匹配机制以及实体类 equals/hashCode 方法在其中的作用。
通过上述方法,您可以有效解决 Mockito save 方法返回 null 的问题,并根据您的测试需求选择最合适的测试策略。
以上就是Mockito save 方法返回 Null 值:深入理解参数匹配与测试策略的详细内容,更多请关注php中文网其它相关文章!
每个人都需要一台速度更快、更稳定的 PC。随着时间的推移,垃圾文件、旧注册表数据和不必要的后台进程会占用资源并降低性能。幸运的是,许多工具可以让 Windows 保持平稳运行。
Copyright 2014-2025 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号