You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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的链式调用匹配未完全覆盖被测试方法中的实际调用逻辑:

  1. 被测试方法中调用sqlClient.createQuery(table)时,table是类内成员变量(而非直接使用UserTable.$),结合RETURNS_DEEP_STUBS的链式调用规则,宽松的参数匹配可能导致mock stub失效;
  2. 被测试方法中对密码做了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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.14 03:07:04