Mockito模拟List返回值失败:JUnit5单元测试遇NPE问题求助
解决SpringBoot中Mockito模拟Jimmer JSqlClient返回List时的NPE问题
问题场景
在SpringBoot项目中使用Mockito结合JUnit5编写UserServiceImpl的用户登录方法单元测试时,需要模拟Jimmer ORM的JSqlClient返回List<UserLoginOutput>,但始终遇到NullPointerException(NPE):
- 最初尝试通过
@Mock模拟List<UserLoginOutput>,让sqlClient返回该实例,调用list.size()时触发NPE; - 改为直接返回
List.of(expectedOutput)后,测试方法内调用sqlClient能正常获取List,但被测试的loginByUsernameAndPassword方法中,sqlClient执行查询得到的list仍为null,调用list.size()再次触发NPE。
注:UserTable.$等价于new UserTable,jSqlClient是通过构造函数注入的private final字段。
原测试代码
@Mock List<UserLoginOutput> queryList; @Mock(answer = Answers.RETURNS_DEEP_STUBS) private RedisTemplate<String, Object> redisTemplate; @Mock(answer = Answers.RETURNS_DEEP_STUBS) private JSqlClient sqlClient; @InjectMocks private UserServiceImpl userService; @BeforeEach public void setUp() { MockitoAnnotations.openMocks(this); } @Test public void loginByUsernameAndPassword_UserFoundAndPasswordCorrect_ReturnsUser() { UserLoginByPasswordInput input = new UserLoginByPasswordInput(); input.setAccount(USER_NAME); input.setPassword(USER_CORRECT_PASSWORD); UserLoginOutput expectedOutput = new UserLoginOutput(); expectedOutput.setId(USER_ID); when(sqlClient .createQuery(UserTable.$) .where((Predicate) any()) .where((Predicate) any()) .select(any()) .execute() ) .thenAnswer( answer -> queryList ); when(queryList.size()).thenReturn(1); UserLoginOutput result = userService.loginByUsernameAndPassword(input); assertEquals(expectedOutput.getId(), result.getId()); }
被测试方法代码
@Override public UserLoginOutput loginByUsernameAndPassword(UserLoginByPasswordInput user) { String encryptedPwd = SHA256Encryption.getSHA256(user.getPassword()); List<UserLoginOutput> list = sqlClient.createQuery(table) .where(table.username().eq(user.getAccount())) .where(table.password().eq(encryptedPwd)) .select( table.fetch( UserLoginOutput.class ) ) .execute(); if (list.size() != 1) { // 此处触发NPE handleLoginFailed(user); } return list.getFirst(); }
报错信息
Cannot invoke "java.util.List.size()" because "list" is null java.lang.NullPointerException: Cannot invoke "java.util.List.size()" because "list" is null at com.bot.backend.service.impl.UserServiceImpl.loginByUsernameAndPassword(UserServiceImpl.java:114) at com.bot.backend.service.impl.UserServiceImplLoginViaPasswordTest.loginByUsernameAndPassword_UserFoundAndPasswordCorrect_ReturnsUser(UserServiceImplLoginViaPasswordTest.java:102) at java.base/java.lang.reflect.Method.invoke(Method.java:580) at java.base/java.util.ArrayList.forEach(ArrayList.java:1596) at java.base/java.util.ArrayList.forEach(ArrayList.java:1596) OpenJDK 64-Bit Server VM warning: Sharing is only supported for boot loader classes because bootstrap classpath has been appended Disconnected from the target VM, address: 'localhost:35341', transport: 'socket' > Task :test FAILED UserServiceImplLoginViaPasswordTest > loginByUsernameAndPassword_UserFoundAndPasswordCorrect_ReturnsUser() FAILED java.lang.NullPointerException at UserServiceImplLoginViaPasswordTest.java:102 1 test completed, 1 failed FAILURE: Build failed with an exception. * What went wrong: Execution failed for task ':test'. > There were failing tests. See the report at: file:///home/napbad/Project/bot-backend/java/build/reports/tests/test/index.html * Try: > Run with --scan to get full insights. BUILD FAILED in 6s 4 actionable tasks: 2 executed, 2 up-to-date
调整后的测试代码(仍未解决问题版本)
@Test public void loginByUsernameAndPassword_UserFoundAndPasswordCorrect_ReturnsUser() { UserLoginByPasswordInput input = new UserLoginByPasswordInput(); input.setAccount("testUser"); input.setPassword("testPassword"); UserLoginOutput expectedOutput = new UserLoginOutput(); expectedOutput.setId(1L); when(sqlClient .createQuery((UserTable) any()) .where((Predicate) any()) .where((Predicate) any()) .select(any()) .execute() ) .thenReturn(List.of(expectedOutput)); String encryptedPwd = SHA256Encryption.getSHA256(input.getPassword()); List<UserLoginOutput> list = sqlClient.createQuery(UserTable.$) .where(UserTable.$.username().eq(input.getAccount())) .where(UserTable.$.password().eq(encryptedPwd)) .select( UserTable.$.fetch( UserLoginOutput.class ) ) .execute(); // 此处可正常获取List UserLoginOutput result = userService.loginByUsernameAndPassword(input); assertEquals(expectedOutput.getId(), result.getId()); }
问题原因
核心问题是Mockito的链式调用匹配未完全覆盖被测试方法中的实际调用逻辑:
- 被测试方法中调用
sqlClient.createQuery(table)时,table是类内成员变量(而非直接使用UserTable.$),结合RETURNS_DEEP_STUBS的链式调用规则,宽松的参数匹配可能导致mock stub失效; - 被测试方法中对密码做了SHA256加密,mock时仅用
any()匹配where条件,但Mockito处理深层stub时,链式调用的每一步参数匹配需准确,否则会返回默认null。
解决方案
方案1:对齐加密密码逻辑,确保参数匹配一致
预先计算加密后的密码,让mock的参数与被测试方法内的实际参数完全匹配:
@Test public void loginByUsernameAndPassword_UserFoundAndPasswordCorrect_ReturnsUser() { UserLoginByPasswordInput input = new UserLoginByPasswordInput(); input.setAccount("testUser"); input.setPassword("testPassword"); // 提前计算加密密码,和被测试方法逻辑保持一致 String encryptedPwd = SHA256Encryption.getSHA256(input.getPassword()); UserLoginOutput expectedOutput = new UserLoginOutput(); expectedOutput.setId(1L); // 针对链式调用做明确匹配,确保stub生效 when(sqlClient.createQuery(any(UserTable.class)) .where(any(Predicate.class)) .where(any(Predicate.class)) .select(any(SelectItem.class)) .execute()) .thenReturn(List.of(expectedOutput)); UserLoginOutput result = userService.loginByUsernameAndPassword(input); assertEquals(expectedOutput.getId(), result.getId()); }
方案2:使用argThat灵活匹配Predicate条件
如果需要验证where条件的具体逻辑,可通过argThat做自定义匹配:
when(sqlClient.createQuery(any(UserTable.class)) .where(argThat(pred -> { // 自定义校验第一个where条件(比如验证是username匹配输入账号) return true; })) .where(argThat(pred -> { // 自定义校验第二个where条件(比如验证是加密密码匹配) return true; })) .select(any()) .execute()) .thenReturn(List.of(expectedOutput));
方案3:避免模拟List,直接返回真实实例
不要用@Mock模拟List,直接返回List.of()或new ArrayList()这类真实List实例,减少不必要的mock环节。
内容的提问来源于stack exchange,提问作者Sen Napbad
相关产品推荐
相关产品推荐

