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

Mockito测试中UserRepository存根参数不匹配致返回Null求助

JUnit5测试UserServiceImpl时Mockito存根匹配失败及交互验证问题排查

问题现象

使用JUnit5 + Mockito测试UserServiceImpl时,出现以下问题:

  • UserRepository无法返回预期值
  • 调用verify(repository, times(1))验证时,提示Mock对象未产生任何交互
  • 测试触发Strict stubbing argument mismatch错误

测试类代码

@ExtendWith(MockitoExtension.class)
public class UserServiceTest {

    @Mock
    UserRepository repository;
    @InjectMocks
    UserServiceImpl userServiceImpl;

    UserModel inputUserModel;

    @BeforeEach
    public void setUp() throws Exception {
        inputUserModel = new UserModel();
        inputUserModel.setEmail("testemail@gmail.com");
        inputUserModel.setFirstName("john");
        inputUserModel.setLastName("doe");
        inputUserModel.setPassword("test");
        inputUserModel.setMatchPassword("test");

        User inputUser = User.builder()
                .email(inputUserModel.getEmail())
                .firstName(inputUserModel.getFirstName())
                .lastName(inputUserModel.getLastName())
                .password(inputUserModel.getPassword())
                .timeCreated(Timestamp.valueOf(LocalDateTime.now(ZoneId.systemDefault())))
                .userID(1)
                .build();

        User outputUser = User.builder()
                .email(inputUserModel.getFirstName())
                .password(inputUserModel.getLastName())
                .lastName(inputUserModel.getFirstName())
                .firstName(inputUserModel.getFirstName())
                .timeCreated(Timestamp.valueOf(LocalDateTime.now(ZoneId.systemDefault())))
                .userID(1)
                .build();

        Mockito.when(repository.save(inputUser)).thenReturn(outputUser);
    }

    @Test
    public void whenSaveUser_ThenUserHasID(){
        Assertions.assertEquals(1, userServiceImpl.saveUser(inputUserModel).getUserID());
    }
}

报错信息

org.mockito.exceptions.misusing.PotentialStubbingProblem: 
Strict stubbing argument mismatch. Please check:
 - this invocation of 'save' method:
    repository.save(
    User(userID=0, email=testemail@gmail.com, timeCreated=2022-12-14 03:18:24.0435578, password=test, firstName=john, lastName=doe)
);
    -> at com.jschwery.securitydemo.service.Implementations.UserServiceImpl.saveUser(UserServiceImpl.java:37)
 - has following stubbing(s) with different arguments:
    1. repository.save(
    User(userID=1, email=testemail@gmail.com, timeCreated=2022-12-14 03:18:24.0235563, password=test, firstName=john, lastName=doe)
);

待测试服务类代码

public class UserServiceImpl implements UserService {

    UserRepository userRepository;

    @Autowired
    public UserServiceImpl(UserRepository repository){
        this.userRepository = repository;
    }

    @Override
    public User saveUser(UserModel userModel) {
        if(!Objects.equals(userModel.getPassword(), userModel.getMatchPassword())){
            throw new UserException("Passwords do not match");
        }
        User user = User.builder()
                .email(userModel.getEmail())
                .firstName(userModel.getFirstName())
                .lastName(userModel.getLastName())
                .password(userModel.getPassword())
                .timeCreated(Timestamp.valueOf(LocalDateTime.now(ZoneId.systemDefault())))
                .build();
        User returnedUser = userRepository.save(user);
        System.out.println(returnedUser.getEmail());
        System.out.println("userID" + returnedUser.getUserID());
        return returnedUser;
    }
}

错误原因分析

从报错信息可明确参数不匹配的核心问题:

  1. UserID不一致:测试类存根的inputUser手动设置了userID=1,但服务类中创建的User对象未指定userID,默认值为0
  2. TimeCreated不一致:测试类存根时的timeCreated是setUp方法执行时的时间,服务类创建User时的timeCreated是saveUser方法执行时的时间,两次时间必然不同
  3. Mockito默认使用对象的equals方法匹配参数,以上两个差异导致存根的save方法无法匹配实际调用,进而引发存根失效、交互验证失败

修复方案

方案1:使用参数匹配器忽略无关属性

将存根逻辑改为使用Mockito参数匹配器,只匹配关键业务属性,忽略userID和timeCreated这类自动生成的属性:

// 替换原存根代码
Mockito.when(repository.save(Mockito.argThat(user -> 
    user.getEmail().equals(inputUserModel.getEmail()) &&
    user.getFirstName().equals(inputUserModel.getFirstName()) &&
    user.getLastName().equals(inputUserModel.getLastName()) &&
    user.getPassword().equals(inputUserModel.getPassword())
))).thenReturn(outputUser);

如果不需要精确匹配属性,也可以直接用any(User.class)简化:

Mockito.when(repository.save(Mockito.any(User.class))).thenReturn(outputUser);

方案2:统一User对象创建逻辑(可选)

将服务类中的User创建逻辑提取为可复用方法,测试时直接复用该方法创建匹配的User对象:

// 服务类中添加方法
protected User buildUserFromModel(UserModel userModel) {
    return User.builder()
            .email(userModel.getEmail())
            .firstName(userModel.getFirstName())
            .lastName(userModel.getLastName())
            .password(userModel.getPassword())
            .timeCreated(Timestamp.valueOf(LocalDateTime.now(ZoneId.systemDefault())))
            .build();
}

// 测试类中调用该方法创建inputUser
User inputUser = userServiceImpl.buildUserFromModel(inputUserModel);
// 此时inputUser的userID为0,和服务类创建的对象一致
Mockito.when(repository.save(inputUser)).thenReturn(outputUser);

注意:这种方式需要处理timeCreated的差异,比如用固定时间替代LocalDateTime.now(),或者在User类中重写equals方法忽略timeCreated属性

方案3:修复verify验证(如果需要)

如果要验证save方法被调用,同样需要使用参数匹配器:

@Test
public void whenSaveUser_ThenUserHasID(){
    User result = userServiceImpl.saveUser(inputUserModel);
    Assertions.assertEquals(1, result.getUserID());
    // 使用参数匹配器验证交互
    Mockito.verify(repository, Mockito.times(1)).save(Mockito.any(User.class));
}

修复后的完整测试类示例

@ExtendWith(MockitoExtension.class)
public class UserServiceTest {

    @Mock
    UserRepository repository;
    @InjectMocks
    UserServiceImpl userServiceImpl;

    UserModel inputUserModel;

    @BeforeEach
    public void setUp() throws Exception {
        inputUserModel = new UserModel();
        inputUserModel.setEmail("testemail@gmail.com");
        inputUserModel.setFirstName("john");
        inputUserModel.setLastName("doe");
        inputUserModel.setPassword("test");
        inputUserModel.setMatchPassword("test");

        User outputUser = User.builder()
                .email(inputUserModel.getFirstName())
                .password(inputUserModel.getLastName())
                .lastName(inputUserModel.getFirstName())
                .firstName(inputUserModel.getFirstName())
                .timeCreated(Timestamp.valueOf(LocalDateTime.now(ZoneId.systemDefault())))
                .userID(1)
                .build();

        // 使用参数匹配器匹配关键属性
        Mockito.when(repository.save(Mockito.argThat(user -> 
            user.getEmail().equals(inputUserModel.getEmail()) &&
            user.getFirstName().equals(inputUserModel.getFirstName()) &&
            user.getLastName().equals(inputUserModel.getLastName()) &&
            user.getPassword().equals(inputUserModel.getPassword())
        ))).thenReturn(outputUser);
    }

    @Test
    public void whenSaveUser_ThenUserHasID(){
        User result = userServiceImpl.saveUser(inputUserModel);
        Assertions.assertEquals(1, result.getUserID());
        // 验证save方法被调用一次
        Mockito.verify(repository, Mockito.times(1)).save(Mockito.any(User.class));
    }
}

内容的提问来源于stack exchange,提问作者Jschwery

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.09 01:01:22