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

如何用JUnit5和Mockito验证局部User对象的方法调用?是否需测试?

如何验证服务类中局部User对象的setPassword方法是否被调用?

问题描述

我有一个针对User实体的服务类,其中包含负责修改用户密码的changeUserPassword方法。该方法会从数据库查询用户并存储在局部变量中,目前功能正常,但我希望编写测试验证User类的密码setter方法是否被调用。

方法代码如下:

public AuthenticationResponse changeUserPassword(ChangeUserPasswordDto changeUserPasswordDto,
            String jwtToken)
            throws RuntimeException
{
        User user = userRepository.findByEmail(jwtUtil.extractUsername(jwtToken)).get();

        if (!passwordEncoder.matches(changeUserPasswordDto.getPassword(),user.getPassword()))
            throw new InvalidPasswordException("Old password is not matching.");
        if (!changeUserPasswordDto.getNewPassword().equals(changeUserPasswordDto.getNewPasswordConfirmation()))
            throw new NotMatchingPasswordsException("Passwords are not matching.");

        user.setPassword(passwordEncoder.encode(changeUserPasswordDto.getNewPassword()));
        user.setLastCredentialsChangeTime(System.currentTimeMillis());
        return AuthenticationResponse.builder().token(jwtUtil.generateToken(userRepository.save(user))).build();
}

我花了半天时间寻找解决方案,仅找到一个使用Powermock库的方案,但该库已有一年未更新,可行性存疑。由于局部变量对外不可见,无法使用Mockito.verify()这类方法进行验证。此外,我也想了解编写此类测试是否有意义?


回答

一、这类测试的意义:优先验证行为结果而非内部细节

首先明确:验证setPassword是否被调用这类测试的实际意义不大。单元测试的核心目标是验证方法的行为结果,而非内部执行细节。

这个changeUserPassword方法的核心职责是:

  • 校验旧密码的正确性
  • 校验新密码与确认密码一致
  • 更新用户的密码和凭证修改时间
  • 返回有效的新JWT Token

你应该聚焦于测试这些最终结果——只要最终持久化的用户密码正确、修改时间已设置,setPassword自然已经被调用。过度关注内部方法调用会让测试耦合到代码实现,后续代码重构(比如把setter调用改成直接字段赋值)会导致测试失效,而实际功能并没有问题。

二、如果一定要验证setter调用:无需Powermock的实现方案

如果出于特殊需求(比如User类的setPassword包含额外业务逻辑,必须确保被执行),可以通过以下方式实现验证,完全不需要依赖Powermock:

1. 让Repository返回User的Spy对象

核心思路是控制userRepository.findByEmail的返回值,让它返回一个你创建的User Spy实例,这样就能捕获这个实例的方法调用:

@Test
void changeUserPassword_ShouldCallSetPassword() {
    // 1. 准备测试数据
    String testEmail = "test@example.com";
    String oldPassword = "old_password_123";
    String newPassword = "new_password_456";
    
    ChangeUserPasswordDto dto = new ChangeUserPasswordDto();
    dto.setPassword(oldPassword);
    dto.setNewPassword(newPassword);
    dto.setNewPasswordConfirmation(newPassword);
    
    String mockJwtToken = "mock_jwt_token";

    // 2. 创建User的Spy对象,并设置加密后的旧密码
    User spyUser = spy(new User());
    spyUser.setPassword(passwordEncoder.encode(oldPassword));

    // 3. Mock依赖组件的行为
    when(jwtUtil.extractUsername(mockJwtToken)).thenReturn(testEmail);
    when(userRepository.findByEmail(testEmail)).thenReturn(Optional.of(spyUser));
    when(userRepository.save(any(User.class))).thenReturn(spyUser);
    when(jwtUtil.generateToken(spyUser)).thenReturn("new_mock_jwt");

    // 4. 执行目标方法
    AuthenticationResponse response = userService.changeUserPassword(dto, mockJwtToken);

    // 5. 验证setter方法被调用
    verify(spyUser).setPassword(anyString());
    verify(spyUser).setLastCredentialsChangeTime(anyLong());
}

2. 通过捕获Repository.save的参数间接验证

这种方式比直接验证setter更有意义,它直接验证了最终要持久化的用户数据是否符合预期:

@Test
void changeUserPassword_ShouldUpdatePasswordCorrectly() {
    // 1. 准备测试数据和Mock依赖(步骤同上)

    // 2. 执行目标方法
    userService.changeUserPassword(dto, mockJwtToken);

    // 3. 捕获save方法的参数,验证密码和修改时间
    ArgumentCaptor<User> userCaptor = ArgumentCaptor.forClass(User.class);
    verify(userRepository).save(userCaptor.capture());
    
    User savedUser = userCaptor.getValue();
    assertTrue(passwordEncoder.matches(newPassword, savedUser.getPassword()));
    assertNotNull(savedUser.getLastCredentialsChangeTime());
}

总结

优先选择验证行为结果的测试用例,避免过度耦合代码实现。如果确实需要验证setter调用,使用Mockito的Spy机制即可,无需依赖维护停滞的Powermock。


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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.17 10:32:33