Search code examples
javaspringunit-testingtestingmockito

when() requires an argument, Mockito and BCrypt


I'm trying to make an unit test in a project, but i'm stuck at trying to mock the BCrypt.checkpw function, i've read that making a static mock requires a certain way to handle the test, i'm not entirely sure if this is the case or maybe is it another thing.

UsuarioController.java

    @GetMapping("lastPassword")
    public ResponseEntity<Object> lastPassword(@RequestParam(required = true) String correo, @RequestParam(required = true) String newPassword) { // Checa si la contraseña ingresada fue la ultima contraseña del usuario 
        try {
            Optional<usuarioVista> tmp = repositoryVista.findByCorreo(correo);
            Optional<Usuario> usuario = service.findById(Integer.toUnsignedLong(tmp.get().getIdUsuario())); // Se busca al usuario perteneciente al correo recibido
            String password = "$2a$14$" + usuario.get().getPassword(); // Se recoge la contraseña encriptada
            Boolean isLast = BCrypt.checkpw(newPassword, password); // Se checa si la contraseña ingresada coincide con la encriptada

            return new ResponseEntity<>(isLast, HttpStatus.OK);
        } catch (Exception e) {
            return ResponseEntity.badRequest().build();
        }
    }

UsuarioControllerTest.java

    @Test
    void lastPassword() throws Exception {
        String correo = "[email protected]";
        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());
    }

Stack trace

%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

I have tried to mock a BCrypt object, use @MockBean annotation but it throws me the same error.


Solution

  • You don't understand very well how mocks work. A mock is done not for a class, but for a specific object of the class. Also, it's not a good idea to mock static methods. If you need to mock a Bcrypt class, I would make a separate service in which I placed this logic and mock the service itself. In your case you don’t even need to create a service, since spring already has BcryptPasswordEncoder.

    Example:

    @RestController
    public YourControllerName {
    
        private final PasswordEncoder passwordEncoder;
        private final RepositoryVista repositoryVista;
        private final UsuarioService service;
    
        public YourControllerName(PasswordEncoder encoder, UsuarioService service, RepositoryVista repositoryVista) {
            this.passwordEncoder = encoder;
            this.service = service;
            this.repositoryVista = repositoryVista;
        }
    
        @GetMapping("lastPassword")
        public ResponseEntity<Object> lastPassword(@RequestParam(required = true) String correo, @RequestParam(required = true) String newPassword) { // Checa si la contraseña ingresada fue la ultima contraseña del usuario 
            try {
                Optional<usuarioVista> tmp = repositoryVista.findByCorreo(correo);
                Optional<Usuario> usuario = service.findById(Integer.toUnsignedLong(tmp.get().getIdUsuario())); // Se busca al usuario perteneciente al correo recibido
                String password = "$2a$14$" + usuario.get().getPassword(); // Se recoge la contraseña encriptada
                Boolean isLast = passwordEncoder.matches(newPassword, password); // Se checa si la contraseña ingresada coincide con la encriptada
    
                return new ResponseEntity<>(isLast, HttpStatus.OK);
            } catch (Exception e) {
                return ResponseEntity.badRequest().build();
            }
        }
    

    To use PasswordEncoder you need to define a bean:

    @Configuration
    puclic class YourConfig {
    
        @Bean
        puclic PasswordEncoder passwordEncoder() {
            return new BcryptPasswordEncoder();
        }
    }
    

    The test in turn will look something like this:

    
       @Autowired
       private YourControllerName controller;
    
       @MockBean
       private PasswordEncoder passwordEncoder
    
    
       @Test
        void lastPassword() throws Exception {
            String correo = "[email protected]";
            String newPassword = "Password123@";
            usuarioVista usuario = new usuarioVista();
            Usuario usuarioOption = mock(Usuario.class);
            Optional<usuarioVista> usuarioCorreo = Optional.of(usuario);
            Optional<Usuario> usuarioId = Optional.of(usuarioOption);
            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(passwordEncoder.matches(newPassword, password)).thenReturn(isLast);
    
            ResponseEntity<Object> response = controller.lastPassword(correo, newPassword);
        
            assertEquals(HttpStatus.OK, response.getStatusCode());
        }