Conteúdo do Curso
Spring Boot Backend
Spring Boot Backend
Mockito
In the previous chapter, we recalled how to write unit tests. However, in our application, this isn't always convenient because testing a controller requires testing the service, and testing the service necessitates testing the repository.
This chain of dependencies raises the question: Do we need to test all modules and their dependencies, or can we simulate their behavior — in other words, mock them?
Mockito: Introduction
Mockito is a popular library for Java that is used to create mock objects in unit tests.
It allows developers to simulate the behavior of complex external dependencies (such as databases, services, or APIs) in order to isolate and test the logic of a specific component (usually a class or method) without interacting with these external systems.
This makes unit testing more predictable and faster since there is no need for real external resource calls during testing.
Mock Objects in Mockito
This allows developers to ensure that the component being tested (the unit) behaves correctly without relying on real dependencies such as databases or third-party services.
A class can be annotated with @Mock
to create its mock version. This annotation is often used alongside @InjectMocks
to automatically inject dependencies into the class under test.
This mock object can then be injected as a dependency into another object using the @InjectMocks
annotation. This annotation automatically injects the mock objects into the class being tested.
Key Methods in Mockito
Mockito provides a wide range of methods for managing mock objects, each serving a specific purpose in the testing process. Below are the most important methods along with their explanations.
Testing Controllers with Mockito
Testing controllers often involves using mocks for services that are called within the controller. In this context, Mockito plays a key role in helping to isolate the controller’s logic from the service layer.
Quick Summary of the Video
We test controllers using the MockMvc
class, but what does it offer and what are the benefits of using it?
With MockMvc
, you can simulate various HTTP requests (GET, POST, PUT, etc.), pass parameters, headers, and verify responses, status codes, headers, and response bodies, making unit testing of controllers much easier.
For example, you can use methods like perform()
to execute the request, andExpect()
to verify the expected results, and content()
to check the content of the response.
Test
mockMvc.perform(put("/books/{id}", bookId) .contentType(MediaType.APPLICATION_JSON) .content(objectMapper.writeValueAsString(bookRequestDTO))) .andExpect(status().isOk())
Additionally, you can chain assertions to check the HTTP status using status().isOk()
, verify JSON response structures with jsonPath()
, and more.
Test
.andExpect(jsonPath("$.id").value(bookResponseDTO.getId())) .andExpect(jsonPath("$.name").value(bookResponseDTO.getName())) .andExpect(jsonPath("$.author").value(bookResponseDTO.getAuthor())) .andExpect(jsonPath("$.price").value(bookResponseDTO.getPrice()));
These tools allow for comprehensive testing of the controller's behavior without actually making real HTTP calls.
Key Annotations
The @WebMvcTest(BookController.class)
annotation is used for testing the BookController
, loading only the components necessary for that specific controller. It excludes the rest of the Spring infrastructure, allowing you to focus on testing the controller itself.
Additionally, the @MockBean
annotation applied to the bookService
field creates a mock version of the service, allowing you to simulate its behavior. This helps isolate the controller from its dependencies and focus on testing its logic.
Tests for the updateBook Method
We wrote two tests for the updateBook
method that cover all possible cases for this method. In the first test, we verify that everything was successful and that our entity was updated.
BookControllerTest
@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); }
This test verifies the successful update of a book
if it exists in the database. The updateBook()
method of the service is called with the book's Id
and a bookRequestDTO
object, after which it returns a bookResponseDTO
object representing the updated book.
Exception Handling Check
We also have a test where an exception occurs in the updateBook
method, and we need to verify how the controller behaves in that situation.
BookControllerTest
@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); }
This test checks the controller’s behavior when attempting to update a book that is not found in the database. The updateBook()
method of the service is first called with the book's Id
and a bookRequestDTO
object.
However, instead of a successful result, the service throws an ApiException
, indicating that the book with the given Id
was not found.
Service Testing with Mockito
When testing a service, it’s important to isolate it from dependencies such as repositories or external APIs. Mockito
allows you to create mocks for these dependencies and define their behavior in the tests.
Brief summary of the video
Let's start by testing our method when it successfully updates the data, meaning the Id
is valid. Obviously, we'll use Mockito
to mock the repository's behavior.
BookServiceTest
@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); }
The method bookRepository.findById("1")
is mocked to return an existing Book
from the repository, and bookRepository.save(bookWithRepository)
is mocked to return the updated book after saving the changes.
The updateBook()
service method is called to update the book based on the bookRequestDTO
, returning a BookResponseDTO
.
assertNotNull(result)
ensures the result isn't null
, indicating a successful update, while assertEquals()
checks that the ID
, name
, author
, and price
of the updated book match the expected values.
Finally, verify()
ensures that findById()
and save()
in the repository were called with the correct parameters.
Checking an Exception in the Service
This test verifies that the service throws an ApiException
if the Book
to be updated is not found in the database.
BookServiceTest
@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)); }
Initially, a BookRequestDTO
object is created with the data for the update, and a test book Id
that does not exist in the database "999"
is assigned. The method bookRepository.findById(idTest)
is mocked to return Optional.empty()
, indicating that there is no book with that Id
.
The test uses the assertThrows()
method to check that when calling bookService.updateBook(idTest, bookRequestDTO)
, an ApiException
is thrown.
Following this, the assertEquals()
methods verify that the exception message matches the expected text and that the error status is equal to HttpStatus.NOT_FOUND
.
The method verify(bookRepository, never()).save(any(Book.class))
ensures that the book saving method save()
was not called, as the book was not found and the update was not performed.
Summary
Mockito
is a library for creating mock objects that allows you to simulate the behavior of dependencies and isolate the code under test. This simplifies testing, making it faster, more stable, and independent of external resources.
Obrigado pelo seu feedback!