Notice: This page requires JavaScript to function properly.
Please enable JavaScript in your browser settings or update your browser.
Impara Mockito | Test delle Applicazioni Backend
Spring Boot Backend

bookMockito

Nel capitolo precedente, abbiamo ricordato come scrivere test unitari. Tuttavia, nella nostra applicazione, questo non è sempre conveniente perché testare un controller richiede di testare il service, e testare il service implica testare il repository.

Questa catena di dipendenze solleva la domanda: dobbiamo testare tutti i moduli e le loro dipendenze, oppure possiamo simulare il loro comportamento — in altre parole, mockarli?

Mockito: Introduzione

Mockito è una popolare libreria per Java utilizzata per creare oggetti mock nei test unitari.

Permette agli sviluppatori di simulare il comportamento di dipendenze esterne complesse (come database, servizi o API) al fine di isolare e testare la logica di uno specifico componente (solitamente una classe o un metodo) senza interagire con questi sistemi esterni.

Questo rende i test unitari più prevedibili e rapidi poiché non è necessario effettuare chiamate a risorse esterne reali durante il testing.

Oggetti Mock in Mockito

Questo consente agli sviluppatori di garantire che il componente in fase di test (l'unità) si comporti correttamente senza dipendere da dipendenze reali come database o servizi di terze parti.

Una classe può essere annotata con @Mock per creare la sua versione mock. Questa annotazione viene spesso utilizzata insieme a @InjectMocks per iniettare automaticamente le dipendenze nella classe sotto test.

@Mock 
private MyService myServiceMock;

Questo oggetto mock può quindi essere iniettato come dipendenza in un altro oggetto utilizzando l'annotazione @InjectMocks. Questa annotazione inietta automaticamente gli oggetti mock nella classe sottoposta a test.

@InjectMocks
private MyController myController;

Metodi chiave in Mockito

Mockito offre una vasta gamma di metodi per la gestione degli oggetti mock, ognuno con uno scopo specifico nel processo di testing. Di seguito sono riportati i metodi più importanti insieme alle loro spiegazioni.

Test dei Controller con Mockito

Il test dei controller spesso prevede l'utilizzo di mock per i servizi invocati all'interno del controller. In questo contesto, Mockito svolge un ruolo chiave nell'isolare la logica del controller dal service layer.

Riepilogo rapido del video

I controller vengono testati utilizzando la classe MockMvc, ma cosa offre e quali sono i vantaggi del suo utilizzo?

@Autowired
private MockMvc mockMvc;

Con MockMvc, è possibile simulare diverse richieste HTTP (GET, POST, PUT, ecc.), passare parametri, header e verificare risposte, codici di stato, header e corpi delle risposte, rendendo il test unitario dei controller molto più semplice.

Ad esempio, è possibile utilizzare metodi come perform() per eseguire la richiesta, andExpect() per verificare i risultati attesi e content() per controllare il contenuto della risposta.

Test.java

Test.java

copy
1234
mockMvc.perform(put("/books/{id}", bookId) .contentType(MediaType.APPLICATION_JSON) .content(objectMapper.writeValueAsString(bookRequestDTO))) .andExpect(status().isOk())

Inoltre, è possibile concatenare asserzioni per verificare lo stato HTTP utilizzando status().isOk(), controllare le strutture della risposta JSON con jsonPath(), e altro ancora.

Test.java

Test.java

copy
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()));

Questi strumenti consentono un test approfondito del comportamento del controller senza effettuare realmente chiamate HTTP.

Annotazioni principali

L'annotazione @WebMvcTest(BookController.class) viene utilizzata per il testing del BookController, caricando solo i componenti necessari per quello specifico controller. Essa esclude il resto dell'infrastruttura Spring, consentendo di concentrare il test sul controller stesso.

@WebMvcTest(BookController.class)
public class BookControllerTest 

Inoltre, l'annotazione @MockBean applicata al campo bookService crea una versione mock del servizio, consentendo di simularne il comportamento. Questo aiuta a isolare il controller dalle sue dipendenze e a concentrarsi sul test della sua logica.

@MockBean
private BookService bookService;

Test per il metodo updateBook

Sono stati scritti due test per il metodo updateBook che coprono tutti i possibili casi per questo metodo. Nel primo test, si verifica che tutto sia andato a buon fine e che la nostra entità sia stata aggiornata.

BookControllerTest.java

BookControllerTest.java

copy
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); }

Questo test verifica l'aggiornamento riuscito di un book se presente nel database. Il metodo updateBook() del service viene chiamato con l'Id del libro e un oggetto bookRequestDTO, dopodiché restituisce un oggetto bookResponseDTO che rappresenta il libro aggiornato.

Verifica della Gestione delle Eccezioni

È presente anche un test in cui si verifica un'eccezione nel metodo updateBook, e occorre verificare come si comporta il controller in tale situazione.

BookControllerTest.java

BookControllerTest.java

copy
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); }

Questo test verifica il comportamento del controller quando si tenta di aggiornare un libro che non è presente nel database. Il metodo updateBook() del service viene chiamato inizialmente con l'Id del libro e un oggetto bookRequestDTO.

Tuttavia, anziché ottenere un risultato positivo, il service genera un'eccezione ApiException, indicando che il libro con l'Id specificato non è stato trovato.

Test dei Service con Mockito

Durante il test di un service, è importante isolarlo dalle dipendenze come repository o API esterne. Mockito consente di creare mock per queste dipendenze e di definirne il comportamento nei test.

Breve riassunto del video

Iniziamo con il test del nostro metodo quando aggiorna con successo i dati, ovvero quando l'Id è valido. Ovviamente, utilizzeremo Mockito per mockare il comportamento del repository.

BookServiceTest.java

BookServiceTest.java

copy
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); }

Il metodo bookRepository.findById("1") viene mockato per restituire un Book esistente dal repository, e bookRepository.save(bookWithRepository) viene mockato per restituire il libro aggiornato dopo aver salvato le modifiche.

Il metodo di servizio updateBook() viene chiamato per aggiornare il libro in base al bookRequestDTO, restituendo un BookResponseDTO.

assertNotNull(result) garantisce che il risultato non sia null, indicando un aggiornamento riuscito, mentre assertEquals() verifica che l'ID, il name, l'author e il price del libro aggiornato corrispondano ai valori attesi.

 assertNotNull(result);
 assertEquals("1", result.getId());
 assertEquals("Updated Name", result.getName());
 assertEquals("Updated Author", result.getAuthor());
 assertEquals("150", result.getPrice());

Infine, verify() garantisce che findById() e save() nel repository siano stati chiamati con i parametri corretti.

verify(bookRepository).findById("1");
verify(bookRepository).save(bookWithRepository);

Verifica di un'eccezione nel servizio

Questo test verifica che il servizio lanci un'ApiException se il Book da aggiornare non viene trovato nel database.

BookServiceTest.java

BookServiceTest.java

copy
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)); }

Inizialmente, viene creato un oggetto BookRequestDTO con i dati per l'aggiornamento, e viene assegnato un Id di test del libro che non esiste nel database "999". Il metodo bookRepository.findById(idTest) viene mockato per restituire Optional.empty(), indicando che non esiste alcun libro con quell'Id.

when(bookRepository.findById(idTest)).thenReturn(Optional.empty());

Il test utilizza il metodo assertThrows() per verificare che, quando si chiama bookService.updateBook(idTest, bookRequestDTO), venga lanciata un'eccezione ApiException.

Successivamente, i metodi assertEquals() verificano che il messaggio dell'eccezione corrisponda al testo atteso e che lo stato di errore sia uguale 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());

Il metodo verify(bookRepository, never()).save(any(Book.class)) assicura che il metodo di salvataggio del libro save() non sia stato chiamato, poiché il libro non è stato trovato e l'aggiornamento non è stato eseguito.

verify(bookRepository, never()).save(any(Book.class));

Riepilogo

Mockito è una libreria per la creazione di oggetti mock che consente di simulare il comportamento delle dipendenze e di isolare il codice sotto test. Questo semplifica i test, rendendoli più rapidi, stabili e indipendenti dalle risorse esterne.

Tutto è chiaro?

Come possiamo migliorarlo?

Grazie per i tuoi commenti!

Sezione 5. Capitolo 3

Chieda ad AI

expand

Chieda ad AI

ChatGPT

Chieda pure quello che desidera o provi una delle domande suggerite per iniziare la nostra conversazione

Suggested prompts:

What are some common mistakes to avoid when using Mockito?

Can you explain the difference between @Mock and @MockBean?

How do I decide when to use mocks versus real objects in my tests?

Awesome!

Completion rate improved to 3.45

bookMockito

Scorri per mostrare il menu

Nel capitolo precedente, abbiamo ricordato come scrivere test unitari. Tuttavia, nella nostra applicazione, questo non è sempre conveniente perché testare un controller richiede di testare il service, e testare il service implica testare il repository.

Questa catena di dipendenze solleva la domanda: dobbiamo testare tutti i moduli e le loro dipendenze, oppure possiamo simulare il loro comportamento — in altre parole, mockarli?

Mockito: Introduzione

Mockito è una popolare libreria per Java utilizzata per creare oggetti mock nei test unitari.

Permette agli sviluppatori di simulare il comportamento di dipendenze esterne complesse (come database, servizi o API) al fine di isolare e testare la logica di uno specifico componente (solitamente una classe o un metodo) senza interagire con questi sistemi esterni.

Questo rende i test unitari più prevedibili e rapidi poiché non è necessario effettuare chiamate a risorse esterne reali durante il testing.

Oggetti Mock in Mockito

Questo consente agli sviluppatori di garantire che il componente in fase di test (l'unità) si comporti correttamente senza dipendere da dipendenze reali come database o servizi di terze parti.

Una classe può essere annotata con @Mock per creare la sua versione mock. Questa annotazione viene spesso utilizzata insieme a @InjectMocks per iniettare automaticamente le dipendenze nella classe sotto test.

@Mock 
private MyService myServiceMock;

Questo oggetto mock può quindi essere iniettato come dipendenza in un altro oggetto utilizzando l'annotazione @InjectMocks. Questa annotazione inietta automaticamente gli oggetti mock nella classe sottoposta a test.

@InjectMocks
private MyController myController;

Metodi chiave in Mockito

Mockito offre una vasta gamma di metodi per la gestione degli oggetti mock, ognuno con uno scopo specifico nel processo di testing. Di seguito sono riportati i metodi più importanti insieme alle loro spiegazioni.

Test dei Controller con Mockito

Il test dei controller spesso prevede l'utilizzo di mock per i servizi invocati all'interno del controller. In questo contesto, Mockito svolge un ruolo chiave nell'isolare la logica del controller dal service layer.

Riepilogo rapido del video

I controller vengono testati utilizzando la classe MockMvc, ma cosa offre e quali sono i vantaggi del suo utilizzo?

@Autowired
private MockMvc mockMvc;

Con MockMvc, è possibile simulare diverse richieste HTTP (GET, POST, PUT, ecc.), passare parametri, header e verificare risposte, codici di stato, header e corpi delle risposte, rendendo il test unitario dei controller molto più semplice.

Ad esempio, è possibile utilizzare metodi come perform() per eseguire la richiesta, andExpect() per verificare i risultati attesi e content() per controllare il contenuto della risposta.

Test.java

Test.java

copy
1234
mockMvc.perform(put("/books/{id}", bookId) .contentType(MediaType.APPLICATION_JSON) .content(objectMapper.writeValueAsString(bookRequestDTO))) .andExpect(status().isOk())

Inoltre, è possibile concatenare asserzioni per verificare lo stato HTTP utilizzando status().isOk(), controllare le strutture della risposta JSON con jsonPath(), e altro ancora.

Test.java

Test.java

copy
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()));

Questi strumenti consentono un test approfondito del comportamento del controller senza effettuare realmente chiamate HTTP.

Annotazioni principali

L'annotazione @WebMvcTest(BookController.class) viene utilizzata per il testing del BookController, caricando solo i componenti necessari per quello specifico controller. Essa esclude il resto dell'infrastruttura Spring, consentendo di concentrare il test sul controller stesso.

@WebMvcTest(BookController.class)
public class BookControllerTest 

Inoltre, l'annotazione @MockBean applicata al campo bookService crea una versione mock del servizio, consentendo di simularne il comportamento. Questo aiuta a isolare il controller dalle sue dipendenze e a concentrarsi sul test della sua logica.

@MockBean
private BookService bookService;

Test per il metodo updateBook

Sono stati scritti due test per il metodo updateBook che coprono tutti i possibili casi per questo metodo. Nel primo test, si verifica che tutto sia andato a buon fine e che la nostra entità sia stata aggiornata.

BookControllerTest.java

BookControllerTest.java

copy
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); }

Questo test verifica l'aggiornamento riuscito di un book se presente nel database. Il metodo updateBook() del service viene chiamato con l'Id del libro e un oggetto bookRequestDTO, dopodiché restituisce un oggetto bookResponseDTO che rappresenta il libro aggiornato.

Verifica della Gestione delle Eccezioni

È presente anche un test in cui si verifica un'eccezione nel metodo updateBook, e occorre verificare come si comporta il controller in tale situazione.

BookControllerTest.java

BookControllerTest.java

copy
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); }

Questo test verifica il comportamento del controller quando si tenta di aggiornare un libro che non è presente nel database. Il metodo updateBook() del service viene chiamato inizialmente con l'Id del libro e un oggetto bookRequestDTO.

Tuttavia, anziché ottenere un risultato positivo, il service genera un'eccezione ApiException, indicando che il libro con l'Id specificato non è stato trovato.

Test dei Service con Mockito

Durante il test di un service, è importante isolarlo dalle dipendenze come repository o API esterne. Mockito consente di creare mock per queste dipendenze e di definirne il comportamento nei test.

Breve riassunto del video

Iniziamo con il test del nostro metodo quando aggiorna con successo i dati, ovvero quando l'Id è valido. Ovviamente, utilizzeremo Mockito per mockare il comportamento del repository.

BookServiceTest.java

BookServiceTest.java

copy
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); }

Il metodo bookRepository.findById("1") viene mockato per restituire un Book esistente dal repository, e bookRepository.save(bookWithRepository) viene mockato per restituire il libro aggiornato dopo aver salvato le modifiche.

Il metodo di servizio updateBook() viene chiamato per aggiornare il libro in base al bookRequestDTO, restituendo un BookResponseDTO.

assertNotNull(result) garantisce che il risultato non sia null, indicando un aggiornamento riuscito, mentre assertEquals() verifica che l'ID, il name, l'author e il price del libro aggiornato corrispondano ai valori attesi.

 assertNotNull(result);
 assertEquals("1", result.getId());
 assertEquals("Updated Name", result.getName());
 assertEquals("Updated Author", result.getAuthor());
 assertEquals("150", result.getPrice());

Infine, verify() garantisce che findById() e save() nel repository siano stati chiamati con i parametri corretti.

verify(bookRepository).findById("1");
verify(bookRepository).save(bookWithRepository);

Verifica di un'eccezione nel servizio

Questo test verifica che il servizio lanci un'ApiException se il Book da aggiornare non viene trovato nel database.

BookServiceTest.java

BookServiceTest.java

copy
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)); }

Inizialmente, viene creato un oggetto BookRequestDTO con i dati per l'aggiornamento, e viene assegnato un Id di test del libro che non esiste nel database "999". Il metodo bookRepository.findById(idTest) viene mockato per restituire Optional.empty(), indicando che non esiste alcun libro con quell'Id.

when(bookRepository.findById(idTest)).thenReturn(Optional.empty());

Il test utilizza il metodo assertThrows() per verificare che, quando si chiama bookService.updateBook(idTest, bookRequestDTO), venga lanciata un'eccezione ApiException.

Successivamente, i metodi assertEquals() verificano che il messaggio dell'eccezione corrisponda al testo atteso e che lo stato di errore sia uguale 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());

Il metodo verify(bookRepository, never()).save(any(Book.class)) assicura che il metodo di salvataggio del libro save() non sia stato chiamato, poiché il libro non è stato trovato e l'aggiornamento non è stato eseguito.

verify(bookRepository, never()).save(any(Book.class));

Riepilogo

Mockito è una libreria per la creazione di oggetti mock che consente di simulare il comportamento delle dipendenze e di isolare il codice sotto test. Questo semplifica i test, rendendoli più rapidi, stabili e indipendenti dalle risorse esterne.

Tutto è chiaro?

Come possiamo migliorarlo?

Grazie per i tuoi commenti!

Sezione 5. Capitolo 3
some-alt