Mockito
En el capítulo anterior, recordamos cómo escribir pruebas unitarias. Sin embargo, en nuestra aplicación, esto no siempre es conveniente porque probar un controlador requiere probar el servicio, y probar el servicio implica probar el repositorio.
Esta cadena de dependencias plantea la pregunta: ¿Es necesario probar todos los módulos y sus dependencias, o podemos simular su comportamiento — en otras palabras, mockearlos?
Mockito: Introducción
Mockito es una biblioteca popular para Java que se utiliza para crear objetos simulados en pruebas unitarias.
Permite a los desarrolladores simular el comportamiento de dependencias externas complejas (como bases de datos, servicios o APIs) para aislar y probar la lógica de un componente específico (usualmente una clase o método) sin interactuar con estos sistemas externos.
Esto hace que las pruebas unitarias sean más predecibles y rápidas, ya que no es necesario realizar llamadas reales a recursos externos durante las pruebas.
Objetos simulados en Mockito
Esto permite a los desarrolladores asegurar que el componente bajo prueba (la unidad) se comporta correctamente sin depender de dependencias reales como bases de datos o servicios de terceros.
Una clase puede ser anotada con @Mock para crear su versión simulada. Esta anotación suele utilizarse junto con @InjectMocks para inyectar automáticamente las dependencias en la clase bajo prueba.
@Mock
private MyService myServiceMock;
Este objeto simulado puede ser inyectado como una dependencia en otro objeto utilizando la anotación @InjectMocks. Esta anotación inyecta automáticamente los objetos simulados en la clase que se está probando.
@InjectMocks
private MyController myController;
Métodos clave en Mockito
Mockito proporciona una amplia gama de métodos para gestionar objetos simulados, cada uno con un propósito específico en el proceso de pruebas. A continuación se presentan los métodos más importantes junto con sus explicaciones.
Pruebas de controladores con Mockito
Las pruebas de controladores suelen implicar el uso de mocks para los servicios que se llaman dentro del controlador. En este contexto, Mockito desempeña un papel clave al ayudar a aislar la lógica del controlador de la capa de servicios.
Resumen rápido del video
Se prueban los controladores utilizando la clase MockMvc, pero ¿qué ofrece y cuáles son los beneficios de utilizarla?
@Autowired
private MockMvc mockMvc;
Con MockMvc, es posible simular diversas solicitudes HTTP (GET, POST, PUT, etc.), pasar parámetros, cabeceras y verificar respuestas, códigos de estado, cabeceras y cuerpos de respuesta, lo que facilita en gran medida las pruebas unitarias de los controladores.
Por ejemplo, se pueden utilizar métodos como perform() para ejecutar la solicitud, andExpect() para verificar los resultados esperados y content() para comprobar el contenido de la respuesta.
Test.java
1234mockMvc.perform(put("/books/{id}", bookId) .contentType(MediaType.APPLICATION_JSON) .content(objectMapper.writeValueAsString(bookRequestDTO))) .andExpect(status().isOk())
Además, es posible encadenar aserciones para comprobar el estado HTTP utilizando status().isOk(), verificar las estructuras de respuesta JSON con jsonPath(), y más.
Test.java
1234.andExpect(jsonPath("$.id").value(bookResponseDTO.getId())) .andExpect(jsonPath("$.name").value(bookResponseDTO.getName())) .andExpect(jsonPath("$.author").value(bookResponseDTO.getAuthor())) .andExpect(jsonPath("$.price").value(bookResponseDTO.getPrice()));
Estas herramientas permiten una prueba integral del comportamiento del controlador sin realizar llamadas HTTP reales.
Anotaciones clave
La anotación @WebMvcTest(BookController.class) se utiliza para probar el BookController, cargando solo los componentes necesarios para ese controlador específico. Excluye el resto de la infraestructura de Spring, permitiendo centrarse en la prueba del propio controlador.
@WebMvcTest(BookController.class)
public class BookControllerTest
Adicionalmente, la anotación @MockBean aplicada al campo bookService crea una versión simulada del servicio, permitiendo simular su comportamiento. Esto ayuda a aislar el controlador de sus dependencias y centrarse en la prueba de su lógica.
@MockBean
private BookService bookService;
Pruebas para el método updateBook
Se escribieron dos pruebas para el método updateBook que cubren todos los casos posibles para este método. En la primera prueba, se verifica que todo fue exitoso y que nuestra entidad fue actualizada.
BookControllerTest.java
1234567891011121314151617@Test void testUpdateBook_whenBookExists_shouldReturnUpdatedBook() throws Exception { String bookId = "1"; when(bookService.updateBook(bookId, bookRequestDTO)).thenReturn(bookResponseDTO); mockMvc.perform(put("/books/{id}", bookId) .contentType(MediaType.APPLICATION_JSON) .content(objectMapper.writeValueAsString(bookRequestDTO))) .andExpect(status().isOk()) .andExpect(jsonPath("$.id").value(bookResponseDTO.getId())) .andExpect(jsonPath("$.name").value(bookResponseDTO.getName())) .andExpect(jsonPath("$.author").value(bookResponseDTO.getAuthor())) .andExpect(jsonPath("$.price").value(bookResponseDTO.getPrice())); verify(bookService).updateBook(bookId, bookRequestDTO); }
Esta prueba verifica la actualización exitosa de un book si existe en la base de datos. Se llama al método updateBook() del servicio con el Id del libro y un objeto bookRequestDTO, tras lo cual devuelve un objeto bookResponseDTO que representa el libro actualizado.
Comprobación del manejo de excepciones
También se dispone de una prueba donde ocurre una excepción en el método updateBook, y es necesario verificar cómo se comporta el controlador en esa situación.
BookControllerTest.java
12345678910111213141516@Test void testUpdateBook_whenBookNotFound_shouldReturnApiException() throws Exception { String bookId = "1"; String errorMessage = "ID not found"; when(bookService.updateBook(bookId, bookRequestDTO)) .thenThrow(new ApiException(errorMessage, HttpStatus.NOT_FOUND)); mockMvc.perform(put("/books/{id}", bookId) .contentType(MediaType.APPLICATION_JSON) .content(objectMapper.writeValueAsString(bookRequestDTO))) .andExpect(status().isNotFound()) .andExpect(jsonPath("$.error").value(errorMessage)); verify(bookService).updateBook(bookId, bookRequestDTO); }
Esta prueba verifica el comportamiento del controlador al intentar actualizar un libro que no se encuentra en la base de datos. El método updateBook() del servicio se llama primero con el Id del libro y un objeto bookRequestDTO.
Sin embargo, en lugar de un resultado exitoso, el servicio lanza una ApiException, indicando que el libro con el Id proporcionado no fue encontrado.
Pruebas de servicios con Mockito
Al probar un servicio, es importante aislarlo de dependencias como repositorios o APIs externas. Mockito permite crear mocks para estas dependencias y definir su comportamiento en las pruebas.
Resumen breve del video
Comencemos por probar nuestro método cuando actualiza correctamente los datos, es decir, cuando el Id es válido. Evidentemente, utilizaremos Mockito para simular el comportamiento del repositorio.
BookServiceTest.java
123456789101112131415161718192021222324252627282930313233@Test void testUpdateBook_whenBookExists_shouldUpdateAndReturnBook() { BookRequestDTO bookRequestDTO = new BookRequestDTO(); bookRequestDTO.setName("Updated Name"); bookRequestDTO.setAuthor("Updated Author"); bookRequestDTO.setPrice("150"); Book bookWithRepository = new Book(); bookWithRepository.setId("1"); bookWithRepository.setName("Original Name"); bookWithRepository.setAuthor("Original Author"); bookWithRepository.setPrice("100"); Book updateBook = new Book(); updateBook.setId("1"); updateBook.setName("Updated Name"); updateBook.setAuthor("Updated Author"); updateBook.setPrice("150"); when(bookRepository.findById("1")).thenReturn(Optional.of(bookWithRepository)); when(bookRepository.save(bookWithRepository)).thenReturn(updateBook); BookResponseDTO result = bookService.updateBook("1", bookRequestDTO); assertNotNull(result); assertEquals("1", result.getId()); assertEquals("Updated Name", result.getName()); assertEquals("Updated Author", result.getAuthor()); assertEquals("150", result.getPrice()); verify(bookRepository).findById("1"); verify(bookRepository).save(bookWithRepository); }
El método bookRepository.findById("1") es simulado para devolver un Book existente del repositorio, y bookRepository.save(bookWithRepository) es simulado para devolver el libro actualizado después de guardar los cambios.
El método de servicio updateBook() se utiliza para actualizar el libro basado en el bookRequestDTO, devolviendo un BookResponseDTO.
assertNotNull(result) asegura que el resultado no sea null, lo que indica una actualización exitosa, mientras que assertEquals() comprueba que el ID, name, author y price del libro actualizado coincidan con los valores esperados.
assertNotNull(result);
assertEquals("1", result.getId());
assertEquals("Updated Name", result.getName());
assertEquals("Updated Author", result.getAuthor());
assertEquals("150", result.getPrice());
Finalmente, verify() asegura que findById() y save() en el repositorio hayan sido llamados con los parámetros correctos.
verify(bookRepository).findById("1");
verify(bookRepository).save(bookWithRepository);
Comprobación de una Excepción en el Servicio
Esta prueba verifica que el servicio lanza una ApiException si el Book que se va a actualizar no se encuentra en la base de datos.
BookServiceTest.java
1234567891011121314151617181920@Test void testUpdateBook_whenBookNotFound_shouldThrowApiException() { BookRequestDTO bookRequestDTO = new BookRequestDTO(); bookRequestDTO.setName("Updated Name"); bookRequestDTO.setAuthor("Updated Author"); bookRequestDTO.setPrice("150"); String idTest = "999"; when(bookRepository.findById(idTest)).thenReturn(Optional.empty()); ApiException apiException = assertThrows(ApiException.class, () -> { bookService.updateBook(idTest, bookRequestDTO); }); assertEquals("Not found book by id: " + idTest, apiException.getMessage()); assertEquals(HttpStatus.NOT_FOUND, apiException.getHttpStatus()); verify(bookRepository, never()).save(any(Book.class)); }
Inicialmente, se crea un objeto BookRequestDTO con los datos para la actualización, y se asigna un Id de libro de prueba que no existe en la base de datos "999". El método bookRepository.findById(idTest) es simulado para devolver Optional.empty(), indicando que no existe un libro con ese Id.
when(bookRepository.findById(idTest)).thenReturn(Optional.empty());
La prueba utiliza el método assertThrows() para verificar que al llamar a bookService.updateBook(idTest, bookRequestDTO), se lanza una ApiException.
Después de esto, los métodos assertEquals() verifican que el mensaje de la excepción coincida con el texto esperado y que el estado de error sea igual a HttpStatus.NOT_FOUND.
ApiException apiException = assertThrows(ApiException.class, () -> {
bookService.updateBook(idTest, bookRequestDTO);
});
assertEquals("Not found book by id: " + idTest, apiException.getMessage());
assertEquals(HttpStatus.NOT_FOUND, apiException.getHttpStatus());
El método verify(bookRepository, never()).save(any(Book.class)) asegura que el método de guardado de libros save() no fue llamado, ya que el libro no fue encontrado y la actualización no se realizó.
verify(bookRepository, never()).save(any(Book.class));
Resumen
Mockito es una librería para crear objetos simulados que permite simular el comportamiento de las dependencias y aislar el código bajo prueba. Esto simplifica las pruebas, haciéndolas más rápidas, más estables e independientes de recursos externos.
¡Gracias por tus comentarios!
Pregunte a AI
Pregunte a AI
Pregunte lo que quiera o pruebe una de las preguntas sugeridas para comenzar nuestra charla
Awesome!
Completion rate improved to 3.45
Mockito
Desliza para mostrar el menú
En el capítulo anterior, recordamos cómo escribir pruebas unitarias. Sin embargo, en nuestra aplicación, esto no siempre es conveniente porque probar un controlador requiere probar el servicio, y probar el servicio implica probar el repositorio.
Esta cadena de dependencias plantea la pregunta: ¿Es necesario probar todos los módulos y sus dependencias, o podemos simular su comportamiento — en otras palabras, mockearlos?
Mockito: Introducción
Mockito es una biblioteca popular para Java que se utiliza para crear objetos simulados en pruebas unitarias.
Permite a los desarrolladores simular el comportamiento de dependencias externas complejas (como bases de datos, servicios o APIs) para aislar y probar la lógica de un componente específico (usualmente una clase o método) sin interactuar con estos sistemas externos.
Esto hace que las pruebas unitarias sean más predecibles y rápidas, ya que no es necesario realizar llamadas reales a recursos externos durante las pruebas.
Objetos simulados en Mockito
Esto permite a los desarrolladores asegurar que el componente bajo prueba (la unidad) se comporta correctamente sin depender de dependencias reales como bases de datos o servicios de terceros.
Una clase puede ser anotada con @Mock para crear su versión simulada. Esta anotación suele utilizarse junto con @InjectMocks para inyectar automáticamente las dependencias en la clase bajo prueba.
@Mock
private MyService myServiceMock;
Este objeto simulado puede ser inyectado como una dependencia en otro objeto utilizando la anotación @InjectMocks. Esta anotación inyecta automáticamente los objetos simulados en la clase que se está probando.
@InjectMocks
private MyController myController;
Métodos clave en Mockito
Mockito proporciona una amplia gama de métodos para gestionar objetos simulados, cada uno con un propósito específico en el proceso de pruebas. A continuación se presentan los métodos más importantes junto con sus explicaciones.
Pruebas de controladores con Mockito
Las pruebas de controladores suelen implicar el uso de mocks para los servicios que se llaman dentro del controlador. En este contexto, Mockito desempeña un papel clave al ayudar a aislar la lógica del controlador de la capa de servicios.
Resumen rápido del video
Se prueban los controladores utilizando la clase MockMvc, pero ¿qué ofrece y cuáles son los beneficios de utilizarla?
@Autowired
private MockMvc mockMvc;
Con MockMvc, es posible simular diversas solicitudes HTTP (GET, POST, PUT, etc.), pasar parámetros, cabeceras y verificar respuestas, códigos de estado, cabeceras y cuerpos de respuesta, lo que facilita en gran medida las pruebas unitarias de los controladores.
Por ejemplo, se pueden utilizar métodos como perform() para ejecutar la solicitud, andExpect() para verificar los resultados esperados y content() para comprobar el contenido de la respuesta.
Test.java
1234mockMvc.perform(put("/books/{id}", bookId) .contentType(MediaType.APPLICATION_JSON) .content(objectMapper.writeValueAsString(bookRequestDTO))) .andExpect(status().isOk())
Además, es posible encadenar aserciones para comprobar el estado HTTP utilizando status().isOk(), verificar las estructuras de respuesta JSON con jsonPath(), y más.
Test.java
1234.andExpect(jsonPath("$.id").value(bookResponseDTO.getId())) .andExpect(jsonPath("$.name").value(bookResponseDTO.getName())) .andExpect(jsonPath("$.author").value(bookResponseDTO.getAuthor())) .andExpect(jsonPath("$.price").value(bookResponseDTO.getPrice()));
Estas herramientas permiten una prueba integral del comportamiento del controlador sin realizar llamadas HTTP reales.
Anotaciones clave
La anotación @WebMvcTest(BookController.class) se utiliza para probar el BookController, cargando solo los componentes necesarios para ese controlador específico. Excluye el resto de la infraestructura de Spring, permitiendo centrarse en la prueba del propio controlador.
@WebMvcTest(BookController.class)
public class BookControllerTest
Adicionalmente, la anotación @MockBean aplicada al campo bookService crea una versión simulada del servicio, permitiendo simular su comportamiento. Esto ayuda a aislar el controlador de sus dependencias y centrarse en la prueba de su lógica.
@MockBean
private BookService bookService;
Pruebas para el método updateBook
Se escribieron dos pruebas para el método updateBook que cubren todos los casos posibles para este método. En la primera prueba, se verifica que todo fue exitoso y que nuestra entidad fue actualizada.
BookControllerTest.java
1234567891011121314151617@Test void testUpdateBook_whenBookExists_shouldReturnUpdatedBook() throws Exception { String bookId = "1"; when(bookService.updateBook(bookId, bookRequestDTO)).thenReturn(bookResponseDTO); mockMvc.perform(put("/books/{id}", bookId) .contentType(MediaType.APPLICATION_JSON) .content(objectMapper.writeValueAsString(bookRequestDTO))) .andExpect(status().isOk()) .andExpect(jsonPath("$.id").value(bookResponseDTO.getId())) .andExpect(jsonPath("$.name").value(bookResponseDTO.getName())) .andExpect(jsonPath("$.author").value(bookResponseDTO.getAuthor())) .andExpect(jsonPath("$.price").value(bookResponseDTO.getPrice())); verify(bookService).updateBook(bookId, bookRequestDTO); }
Esta prueba verifica la actualización exitosa de un book si existe en la base de datos. Se llama al método updateBook() del servicio con el Id del libro y un objeto bookRequestDTO, tras lo cual devuelve un objeto bookResponseDTO que representa el libro actualizado.
Comprobación del manejo de excepciones
También se dispone de una prueba donde ocurre una excepción en el método updateBook, y es necesario verificar cómo se comporta el controlador en esa situación.
BookControllerTest.java
12345678910111213141516@Test void testUpdateBook_whenBookNotFound_shouldReturnApiException() throws Exception { String bookId = "1"; String errorMessage = "ID not found"; when(bookService.updateBook(bookId, bookRequestDTO)) .thenThrow(new ApiException(errorMessage, HttpStatus.NOT_FOUND)); mockMvc.perform(put("/books/{id}", bookId) .contentType(MediaType.APPLICATION_JSON) .content(objectMapper.writeValueAsString(bookRequestDTO))) .andExpect(status().isNotFound()) .andExpect(jsonPath("$.error").value(errorMessage)); verify(bookService).updateBook(bookId, bookRequestDTO); }
Esta prueba verifica el comportamiento del controlador al intentar actualizar un libro que no se encuentra en la base de datos. El método updateBook() del servicio se llama primero con el Id del libro y un objeto bookRequestDTO.
Sin embargo, en lugar de un resultado exitoso, el servicio lanza una ApiException, indicando que el libro con el Id proporcionado no fue encontrado.
Pruebas de servicios con Mockito
Al probar un servicio, es importante aislarlo de dependencias como repositorios o APIs externas. Mockito permite crear mocks para estas dependencias y definir su comportamiento en las pruebas.
Resumen breve del video
Comencemos por probar nuestro método cuando actualiza correctamente los datos, es decir, cuando el Id es válido. Evidentemente, utilizaremos Mockito para simular el comportamiento del repositorio.
BookServiceTest.java
123456789101112131415161718192021222324252627282930313233@Test void testUpdateBook_whenBookExists_shouldUpdateAndReturnBook() { BookRequestDTO bookRequestDTO = new BookRequestDTO(); bookRequestDTO.setName("Updated Name"); bookRequestDTO.setAuthor("Updated Author"); bookRequestDTO.setPrice("150"); Book bookWithRepository = new Book(); bookWithRepository.setId("1"); bookWithRepository.setName("Original Name"); bookWithRepository.setAuthor("Original Author"); bookWithRepository.setPrice("100"); Book updateBook = new Book(); updateBook.setId("1"); updateBook.setName("Updated Name"); updateBook.setAuthor("Updated Author"); updateBook.setPrice("150"); when(bookRepository.findById("1")).thenReturn(Optional.of(bookWithRepository)); when(bookRepository.save(bookWithRepository)).thenReturn(updateBook); BookResponseDTO result = bookService.updateBook("1", bookRequestDTO); assertNotNull(result); assertEquals("1", result.getId()); assertEquals("Updated Name", result.getName()); assertEquals("Updated Author", result.getAuthor()); assertEquals("150", result.getPrice()); verify(bookRepository).findById("1"); verify(bookRepository).save(bookWithRepository); }
El método bookRepository.findById("1") es simulado para devolver un Book existente del repositorio, y bookRepository.save(bookWithRepository) es simulado para devolver el libro actualizado después de guardar los cambios.
El método de servicio updateBook() se utiliza para actualizar el libro basado en el bookRequestDTO, devolviendo un BookResponseDTO.
assertNotNull(result) asegura que el resultado no sea null, lo que indica una actualización exitosa, mientras que assertEquals() comprueba que el ID, name, author y price del libro actualizado coincidan con los valores esperados.
assertNotNull(result);
assertEquals("1", result.getId());
assertEquals("Updated Name", result.getName());
assertEquals("Updated Author", result.getAuthor());
assertEquals("150", result.getPrice());
Finalmente, verify() asegura que findById() y save() en el repositorio hayan sido llamados con los parámetros correctos.
verify(bookRepository).findById("1");
verify(bookRepository).save(bookWithRepository);
Comprobación de una Excepción en el Servicio
Esta prueba verifica que el servicio lanza una ApiException si el Book que se va a actualizar no se encuentra en la base de datos.
BookServiceTest.java
1234567891011121314151617181920@Test void testUpdateBook_whenBookNotFound_shouldThrowApiException() { BookRequestDTO bookRequestDTO = new BookRequestDTO(); bookRequestDTO.setName("Updated Name"); bookRequestDTO.setAuthor("Updated Author"); bookRequestDTO.setPrice("150"); String idTest = "999"; when(bookRepository.findById(idTest)).thenReturn(Optional.empty()); ApiException apiException = assertThrows(ApiException.class, () -> { bookService.updateBook(idTest, bookRequestDTO); }); assertEquals("Not found book by id: " + idTest, apiException.getMessage()); assertEquals(HttpStatus.NOT_FOUND, apiException.getHttpStatus()); verify(bookRepository, never()).save(any(Book.class)); }
Inicialmente, se crea un objeto BookRequestDTO con los datos para la actualización, y se asigna un Id de libro de prueba que no existe en la base de datos "999". El método bookRepository.findById(idTest) es simulado para devolver Optional.empty(), indicando que no existe un libro con ese Id.
when(bookRepository.findById(idTest)).thenReturn(Optional.empty());
La prueba utiliza el método assertThrows() para verificar que al llamar a bookService.updateBook(idTest, bookRequestDTO), se lanza una ApiException.
Después de esto, los métodos assertEquals() verifican que el mensaje de la excepción coincida con el texto esperado y que el estado de error sea igual a HttpStatus.NOT_FOUND.
ApiException apiException = assertThrows(ApiException.class, () -> {
bookService.updateBook(idTest, bookRequestDTO);
});
assertEquals("Not found book by id: " + idTest, apiException.getMessage());
assertEquals(HttpStatus.NOT_FOUND, apiException.getHttpStatus());
El método verify(bookRepository, never()).save(any(Book.class)) asegura que el método de guardado de libros save() no fue llamado, ya que el libro no fue encontrado y la actualización no se realizó.
verify(bookRepository, never()).save(any(Book.class));
Resumen
Mockito es una librería para crear objetos simulados que permite simular el comportamiento de las dependencias y aislar el código bajo prueba. Esto simplifica las pruebas, haciéndolas más rápidas, más estables e independientes de recursos externos.
¡Gracias por tus comentarios!