Mockito测试报错:when()需传入mock方法调用参数(BCrypt问题)
问题:模拟BCrypt.checkpw静态方法时抛出MissingMethodInvocationException
编写单元测试尝试模拟BCrypt.checkpw静态方法时,抛出MissingMethodInvocationException,提示when()需传入mock对象的方法调用参数。尝试过直接mock BCrypt对象、使用@MockBean注解,均未解决问题。
相关代码
UsuarioController.java
@GetMapping("lastPassword") public ResponseEntity<Object> lastPassword(@RequestParam(required = true) String correo, @RequestParam(required = true) String newPassword) { // 检查输入的密码是否为用户最后一次使用的密码 try { Optional<usuarioVista> tmp = repositoryVista.findByCorreo(correo); Optional<Usuario> usuario = service.findById(Integer.toUnsignedLong(tmp.get().getIdUsuario())); // 根据收到的邮箱查找用户 String password = "$2a$14$" + usuario.get().getPassword(); // 获取加密后的密码 Boolean isLast = BCrypt.checkpw(newPassword, password); // 检查输入的密码是否与加密密码匹配 return new ResponseEntity<>(isLast, HttpStatus.OK); } catch (Exception e) { return ResponseEntity.badRequest().build(); } }
UsuarioControllerTest.java
@Test void lastPassword() throws Exception { String correo = "example@gmail.com"; String newPassword = "Password123@"; usuarioVista usuario = new usuarioVista(); Usuario usuarioOption = mock(Usuario.class); Optional<usuarioVista> usuarioCorreo = Optional.of(usuario); Optional<Usuario> usuarioId = Optional.of(usuarioOption); BCrypt encriptado = mock(BCrypt.class); String password = "$2a$14$9SXpfOnx/waQlmHn6L3yp.L5d7wlf.ZMqVIwaDHuq8KDhppWtHi9K"; Boolean isLast = true; Mockito.when(repositoryVista.findByCorreo(correo)).thenReturn(usuarioCorreo); Mockito.when(usuarioService.findById(33L)).thenReturn(usuarioId); Mockito.when(usuarioOption.getPassword()).thenReturn(newPassword); when(encriptado.checkpw(newPassword, password)).thenReturn(isLast); ResponseEntity<Object> response = controller.lastPassword(correo, newPassword); assertEquals(HttpStatus.OK, response.getStatusCode()); }
错误堆栈信息
%TESTC 1 v2 %TSTTREE2,mx.com.ficachi.master.login.controllers.UsuarioControllerTest,true,1,false,1,UsuarioControllerTest,,[engine:junit-jupiter]/[class:mx.com.ficachi.master.login.controllers.UsuarioControllerTest] %TSTTREE3,lastPassword(mx.com.ficachi.master.login.controllers.UsuarioControllerTest),false,1,false,2,lastPassword(),,[engine:junit-jupiter]/[class:mx.com.ficachi.master.login.controllers.UsuarioControllerTest]/[method:lastPassword()] %TESTS 3,lastPassword(mx.com.ficachi.master.login.controllers.UsuarioControllerTest) %ERROR 3,lastPassword(mx.com.ficachi.master.login.controllers.UsuarioControllerTest) %TRACES org.mockito.exceptions.misusing.MissingMethodInvocationException: when() requires an argument which has to be 'a method call on a mock'. For example: when(mock.getArticles()).thenReturn(articles); Also, this error might show up because: 1. you stub either of: final/private/equals()/hashCode() methods. Those methods *cannot* be stubbed/verified. Mocking methods declared on non-public parent classes is not supported. 2. inside when() you don't call method on mock but on some other object. at mx.com.ficachi.master.login.controllers.UsuarioControllerTest.lastPassword(UsuarioControllerTest.java:148) at java.base/java.lang.reflect.Method.invoke(Method.java:568) at java.base/java.util.ArrayList.forEach(ArrayList.java:1511) at java.base/java.util.ArrayList.forEach(ArrayList.java:1511) %TRACEE %TESTE 3,lastPassword(mx.com.ficachi.master.login.controllers.UsuarioControllerTest) %RUNTIME8003
解决方案
原因分析
原测试代码中mock的是BCrypt类的实例,但Controller中调用的是静态方法BCrypt.checkpw(),两者没有关联,导致mock失效。普通Mockito无法直接mock静态方法,需使用静态mock功能或封装静态调用。
方法1:使用Mockito静态mock(适用于Mockito 3.4+)
通过Mockito.mockStatic()创建BCrypt类的静态mock,限定作用域在try-with-resources块内:
修改后的测试代码:
@Test void lastPassword() throws Exception { String correo = "example@gmail.com"; String newPassword = "Password123@"; usuarioVista usuario = new usuarioVista(); usuario.setIdUsuario(33); // 补充设置id,避免后续转换异常 Usuario usuarioOption = mock(Usuario.class); Optional<usuarioVista> usuarioCorreo = Optional.of(usuario); Optional<Usuario> usuarioId = Optional.of(usuarioOption); String storedPasswordSuffix = "encryptedPasswordSuffix"; String fullEncryptedPassword = "$2a$14$" + storedPasswordSuffix; Boolean isLast = true; Mockito.when(repositoryVista.findByCorreo(correo)).thenReturn(usuarioCorreo); Mockito.when(usuarioService.findById(33L)).thenReturn(usuarioId); Mockito.when(usuarioOption.getPassword()).thenReturn(storedPasswordSuffix); // 静态mock BCrypt类,指定checkpw的行为 try (MockedStatic<BCrypt> mockedBCrypt = Mockito.mockStatic(BCrypt.class)) { mockedBCrypt.when(() -> BCrypt.checkpw(newPassword, fullEncryptedPassword)) .thenReturn(isLast); ResponseEntity<Object> response = controller.lastPassword(correo, newPassword); assertEquals(HttpStatus.OK, response.getStatusCode()); assertEquals(isLast, response.getBody()); } }
方法2:封装静态调用为服务类(推荐,符合依赖注入原则)
将BCrypt的静态调用封装到一个独立的服务类中,通过依赖注入在Controller中使用,测试时mock该服务类即可。
- 创建密码验证服务类:
@Service public class PasswordValidationService { public boolean checkPassword(String rawPassword, String encryptedPassword) { return BCrypt.checkpw(rawPassword, encryptedPassword); } }
- 修改Controller注入该服务:
@RestController public class UsuarioController { private final PasswordValidationService passwordValidationService; private final UsuarioRepositoryVista repositoryVista; private final UsuarioService service; // 构造函数注入服务 public UsuarioController(PasswordValidationService passwordValidationService, UsuarioRepositoryVista repositoryVista, UsuarioService service) { this.passwordValidationService = passwordValidationService; this.repositoryVista = repositoryVista; this.service = service; } @GetMapping("lastPassword") public ResponseEntity<Object> lastPassword(@RequestParam(required = true) String correo, @RequestParam(required = true) String newPassword) { try { Optional<usuarioVista> tmp = repositoryVista.findByCorreo(correo); Optional<Usuario> usuario = service.findById(Integer.toUnsignedLong(tmp.get().getIdUsuario())); String password = "$2a$14$" + usuario.get().getPassword(); Boolean isLast = passwordValidationService.checkPassword(newPassword, password); // 使用服务类调用 return new ResponseEntity<>(isLast, HttpStatus.OK); } catch (Exception e) { return ResponseEntity.badRequest().build(); } } }
- 修改测试代码mock服务类:
@ExtendWith(MockitoExtension.class) public class UsuarioControllerTest { @InjectMocks private UsuarioController controller; @Mock private UsuarioRepositoryVista repositoryVista; @Mock private UsuarioService usuarioService; @Mock private PasswordValidationService passwordValidationService; // mock服务类 @Test void lastPassword() throws Exception { String correo = "example@gmail.com"; String newPassword = "Password123@"; usuarioVista usuario = new usuarioVista(); usuario.setIdUsuario(33); Usuario usuarioOption = mock(Usuario.class); Optional<usuarioVista> usuarioCorreo = Optional.of(usuario); Optional<Usuario> usuarioId = Optional.of(usuarioOption); String storedPasswordSuffix = "encryptedPasswordSuffix"; String fullEncryptedPassword = "$2a$14$" + storedPasswordSuffix; Boolean isLast = true; Mockito.when(repositoryVista.findByCorreo(correo)).thenReturn(usuarioCorreo); Mockito.when(usuarioService.findById(33L)).thenReturn(usuarioId); Mockito.when(usuarioOption.getPassword()).thenReturn(storedPasswordSuffix); Mockito.when(passwordValidationService.checkPassword(newPassword, fullEncryptedPassword)) .thenReturn(isLast); ResponseEntity<Object> response = controller.lastPassword(correo, newPassword); assertEquals(HttpStatus.OK, response.getStatusCode()); assertEquals(isLast, response.getBody()); } }
内容的提问来源于stack exchange,提问作者Axel Fernando Castro Jimenez
相关产品推荐
相关产品推荐

