如何用JUnit5和Mockito验证局部User对象的方法调用?是否需测试?
问题描述
我有一个针对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

