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

bookMockito

Dans le chapitre précédent, nous avons rappelé comment rédiger des tests unitaires. Cependant, dans notre application, cela n'est pas toujours pratique car tester un contrôleur implique de tester le service, et tester le service nécessite de tester le repository.

Cette chaîne de dépendances soulève la question suivante : devons-nous tester tous les modules et leurs dépendances, ou pouvons-nous simuler leur comportement — en d'autres termes, les mock ?

Mockito : Introduction

Mockito est une bibliothèque populaire pour Java utilisée afin de créer des objets mock dans les tests unitaires.

Elle permet aux développeurs de simuler le comportement de dépendances externes complexes (telles que des bases de données, des services ou des API) afin d'isoler et de tester la logique d'un composant spécifique (généralement une classe ou une méthode) sans interagir avec ces systèmes externes.

Cela rend les tests unitaires plus prévisibles et rapides puisqu'il n'est pas nécessaire d'effectuer de véritables appels à des ressources externes lors des tests.

Objets simulés dans Mockito

Cela permet aux développeurs de s'assurer que le composant testé (l'unité) se comporte correctement sans dépendre de dépendances réelles telles que les bases de données ou des services tiers.

Une classe peut être annotée avec @Mock pour créer sa version simulée. Cette annotation est souvent utilisée avec @InjectMocks pour injecter automatiquement les dépendances dans la classe à tester.

@Mock 
private MyService myServiceMock;

Cet objet simulé peut ensuite être injecté comme dépendance dans un autre objet à l'aide de l'annotation @InjectMocks. Cette annotation injecte automatiquement les objets simulés dans la classe testée.

@InjectMocks
private MyController myController;

Méthodes clés dans Mockito

Mockito propose un large éventail de méthodes pour gérer les objets simulés, chacune ayant un objectif spécifique dans le processus de test. Voici les méthodes les plus importantes accompagnées de leurs explications.

Tester les contrôleurs avec Mockito

Les tests des contrôleurs impliquent souvent l’utilisation de mocks pour les services appelés au sein du contrôleur. Dans ce contexte, Mockito joue un rôle clé en aidant à isoler la logique du contrôleur de la couche service.

Résumé rapide de la vidéo

Les contrôleurs sont testés à l’aide de la classe MockMvc, mais qu’offre-t-elle et quels sont les avantages de son utilisation ?

@Autowired
private MockMvc mockMvc;

Avec MockMvc, possibilité de simuler différentes requêtes HTTP (GET, POST, PUT, etc.), de transmettre des paramètres, des en-têtes, et de vérifier les réponses, les codes d'état, les en-têtes et les corps de réponse, ce qui rend les tests unitaires des contrôleurs beaucoup plus simples.

Par exemple, utilisation de méthodes telles que perform() pour exécuter la requête, andExpect() pour vérifier les résultats attendus, et content() pour contrôler le contenu de la réponse.

Test.java

Test.java

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

De plus, possibilité de chaîner les assertions pour vérifier le statut HTTP avec status().isOk(), contrôler la structure de la réponse JSON via jsonPath(), et bien d'autres options.

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

Ces outils permettent une vérification complète du comportement du contrôleur sans effectuer de véritables appels HTTP.

Annotations clés

L’annotation @WebMvcTest(BookController.class) est utilisée pour les tests du BookController, en chargeant uniquement les composants nécessaires à ce contrôleur spécifique. Elle exclut le reste de l’infrastructure Spring, permettant de se concentrer sur le test du contrôleur lui-même.

@WebMvcTest(BookController.class)
public class BookControllerTest 

De plus, l’annotation @MockBean appliquée au champ bookService crée une version simulée du service, permettant de simuler son comportement. Cela aide à isoler le contrôleur de ses dépendances et à se concentrer sur le test de sa logique.

@MockBean
private BookService bookService;

Tests pour la méthode updateBook

Nous avons rédigé deux tests pour la méthode updateBook qui couvrent tous les cas possibles pour cette méthode. Dans le premier test, nous vérifions que tout s’est bien passé et que notre entité a été mise à jour.

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

Ce test vérifie la mise à jour réussie d’un book s’il existe dans la base de données. La méthode updateBook() du service est appelée avec l’Id du livre et un objet bookRequestDTO, après quoi elle retourne un objet bookResponseDTO représentant le livre mis à jour.

Vérification de la gestion des exceptions

Nous avons également un test où une exception se produit dans la méthode updateBook, et nous devons vérifier le comportement du contrôleur dans cette situation.

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

Ce test vérifie le comportement du contrôleur lors d'une tentative de mise à jour d'un livre qui est introuvable dans la base de données. La méthode updateBook() du service est d'abord appelée avec l'Id du livre et un objet bookRequestDTO.

Cependant, au lieu d'obtenir un résultat réussi, le service lève une ApiException, indiquant que le livre avec l'Id donné est introuvable.

Test de service avec Mockito

Lors du test d'un service, il est important de l'isoler de ses dépendances telles que les référentiels ou les API externes. Mockito permet de créer des mocks pour ces dépendances et de définir leur comportement dans les tests.

Résumé bref de la vidéo

Commençons par tester notre méthode lorsqu'elle met à jour avec succès les données, c'est-à-dire lorsque l'Id est valide. Évidemment, nous utiliserons Mockito pour simuler le comportement du 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); }

La méthode bookRepository.findById("1") est simulée pour retourner un Book existant du repository, et bookRepository.save(bookWithRepository) est simulée pour retourner le livre mis à jour après l'enregistrement des modifications.

La méthode de service updateBook() est appelée pour mettre à jour le livre en fonction du bookRequestDTO, et retourne un BookResponseDTO.

assertNotNull(result) garantit que le résultat n'est pas null, ce qui indique une mise à jour réussie, tandis que assertEquals() vérifie que l'ID, le name, l'author et le price du livre mis à jour correspondent aux valeurs attendues.

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

Enfin, verify() garantit que findById() et save() dans le repository ont été appelés avec les bons paramètres.

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

Vérification d'une exception dans le service

Ce test vérifie que le service lève une ApiException si le Book à mettre à jour n'est pas trouvé dans la base de données.

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

Initialement, un objet BookRequestDTO est créé avec les données pour la mise à jour, et un identifiant de livre de test Id qui n'existe pas dans la base de données "999" est attribué. La méthode bookRepository.findById(idTest) est simulée pour retourner Optional.empty(), indiquant qu'il n'existe aucun livre avec cet Id.

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

Le test utilise la méthode assertThrows() pour vérifier que lors de l'appel de bookService.updateBook(idTest, bookRequestDTO), une ApiException est levée.

Ensuite, les méthodes assertEquals() vérifient que le message d'exception correspond au texte attendu et que le statut d'erreur est égal à 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());

La méthode verify(bookRepository, never()).save(any(Book.class)) garantit que la méthode d'enregistrement du livre save() n'a pas été appelée, car le livre n'a pas été trouvé et la mise à jour n'a pas été effectuée.

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

Résumé

Mockito est une bibliothèque permettant de créer des objets simulés qui offre la possibilité de simuler le comportement des dépendances et d'isoler le code testé. Cela simplifie les tests, les rend plus rapides, plus stables et indépendants des ressources externes.

Tout était clair ?

Comment pouvons-nous l'améliorer ?

Merci pour vos commentaires !

Section 5. Chapitre 3

Demandez à l'IA

expand

Demandez à l'IA

ChatGPT

Posez n'importe quelle question ou essayez l'une des questions suggérées pour commencer notre discussion

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

Glissez pour afficher le menu

Dans le chapitre précédent, nous avons rappelé comment rédiger des tests unitaires. Cependant, dans notre application, cela n'est pas toujours pratique car tester un contrôleur implique de tester le service, et tester le service nécessite de tester le repository.

Cette chaîne de dépendances soulève la question suivante : devons-nous tester tous les modules et leurs dépendances, ou pouvons-nous simuler leur comportement — en d'autres termes, les mock ?

Mockito : Introduction

Mockito est une bibliothèque populaire pour Java utilisée afin de créer des objets mock dans les tests unitaires.

Elle permet aux développeurs de simuler le comportement de dépendances externes complexes (telles que des bases de données, des services ou des API) afin d'isoler et de tester la logique d'un composant spécifique (généralement une classe ou une méthode) sans interagir avec ces systèmes externes.

Cela rend les tests unitaires plus prévisibles et rapides puisqu'il n'est pas nécessaire d'effectuer de véritables appels à des ressources externes lors des tests.

Objets simulés dans Mockito

Cela permet aux développeurs de s'assurer que le composant testé (l'unité) se comporte correctement sans dépendre de dépendances réelles telles que les bases de données ou des services tiers.

Une classe peut être annotée avec @Mock pour créer sa version simulée. Cette annotation est souvent utilisée avec @InjectMocks pour injecter automatiquement les dépendances dans la classe à tester.

@Mock 
private MyService myServiceMock;

Cet objet simulé peut ensuite être injecté comme dépendance dans un autre objet à l'aide de l'annotation @InjectMocks. Cette annotation injecte automatiquement les objets simulés dans la classe testée.

@InjectMocks
private MyController myController;

Méthodes clés dans Mockito

Mockito propose un large éventail de méthodes pour gérer les objets simulés, chacune ayant un objectif spécifique dans le processus de test. Voici les méthodes les plus importantes accompagnées de leurs explications.

Tester les contrôleurs avec Mockito

Les tests des contrôleurs impliquent souvent l’utilisation de mocks pour les services appelés au sein du contrôleur. Dans ce contexte, Mockito joue un rôle clé en aidant à isoler la logique du contrôleur de la couche service.

Résumé rapide de la vidéo

Les contrôleurs sont testés à l’aide de la classe MockMvc, mais qu’offre-t-elle et quels sont les avantages de son utilisation ?

@Autowired
private MockMvc mockMvc;

Avec MockMvc, possibilité de simuler différentes requêtes HTTP (GET, POST, PUT, etc.), de transmettre des paramètres, des en-têtes, et de vérifier les réponses, les codes d'état, les en-têtes et les corps de réponse, ce qui rend les tests unitaires des contrôleurs beaucoup plus simples.

Par exemple, utilisation de méthodes telles que perform() pour exécuter la requête, andExpect() pour vérifier les résultats attendus, et content() pour contrôler le contenu de la réponse.

Test.java

Test.java

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

De plus, possibilité de chaîner les assertions pour vérifier le statut HTTP avec status().isOk(), contrôler la structure de la réponse JSON via jsonPath(), et bien d'autres options.

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

Ces outils permettent une vérification complète du comportement du contrôleur sans effectuer de véritables appels HTTP.

Annotations clés

L’annotation @WebMvcTest(BookController.class) est utilisée pour les tests du BookController, en chargeant uniquement les composants nécessaires à ce contrôleur spécifique. Elle exclut le reste de l’infrastructure Spring, permettant de se concentrer sur le test du contrôleur lui-même.

@WebMvcTest(BookController.class)
public class BookControllerTest 

De plus, l’annotation @MockBean appliquée au champ bookService crée une version simulée du service, permettant de simuler son comportement. Cela aide à isoler le contrôleur de ses dépendances et à se concentrer sur le test de sa logique.

@MockBean
private BookService bookService;

Tests pour la méthode updateBook

Nous avons rédigé deux tests pour la méthode updateBook qui couvrent tous les cas possibles pour cette méthode. Dans le premier test, nous vérifions que tout s’est bien passé et que notre entité a été mise à jour.

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

Ce test vérifie la mise à jour réussie d’un book s’il existe dans la base de données. La méthode updateBook() du service est appelée avec l’Id du livre et un objet bookRequestDTO, après quoi elle retourne un objet bookResponseDTO représentant le livre mis à jour.

Vérification de la gestion des exceptions

Nous avons également un test où une exception se produit dans la méthode updateBook, et nous devons vérifier le comportement du contrôleur dans cette situation.

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

Ce test vérifie le comportement du contrôleur lors d'une tentative de mise à jour d'un livre qui est introuvable dans la base de données. La méthode updateBook() du service est d'abord appelée avec l'Id du livre et un objet bookRequestDTO.

Cependant, au lieu d'obtenir un résultat réussi, le service lève une ApiException, indiquant que le livre avec l'Id donné est introuvable.

Test de service avec Mockito

Lors du test d'un service, il est important de l'isoler de ses dépendances telles que les référentiels ou les API externes. Mockito permet de créer des mocks pour ces dépendances et de définir leur comportement dans les tests.

Résumé bref de la vidéo

Commençons par tester notre méthode lorsqu'elle met à jour avec succès les données, c'est-à-dire lorsque l'Id est valide. Évidemment, nous utiliserons Mockito pour simuler le comportement du 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); }

La méthode bookRepository.findById("1") est simulée pour retourner un Book existant du repository, et bookRepository.save(bookWithRepository) est simulée pour retourner le livre mis à jour après l'enregistrement des modifications.

La méthode de service updateBook() est appelée pour mettre à jour le livre en fonction du bookRequestDTO, et retourne un BookResponseDTO.

assertNotNull(result) garantit que le résultat n'est pas null, ce qui indique une mise à jour réussie, tandis que assertEquals() vérifie que l'ID, le name, l'author et le price du livre mis à jour correspondent aux valeurs attendues.

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

Enfin, verify() garantit que findById() et save() dans le repository ont été appelés avec les bons paramètres.

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

Vérification d'une exception dans le service

Ce test vérifie que le service lève une ApiException si le Book à mettre à jour n'est pas trouvé dans la base de données.

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

Initialement, un objet BookRequestDTO est créé avec les données pour la mise à jour, et un identifiant de livre de test Id qui n'existe pas dans la base de données "999" est attribué. La méthode bookRepository.findById(idTest) est simulée pour retourner Optional.empty(), indiquant qu'il n'existe aucun livre avec cet Id.

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

Le test utilise la méthode assertThrows() pour vérifier que lors de l'appel de bookService.updateBook(idTest, bookRequestDTO), une ApiException est levée.

Ensuite, les méthodes assertEquals() vérifient que le message d'exception correspond au texte attendu et que le statut d'erreur est égal à 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());

La méthode verify(bookRepository, never()).save(any(Book.class)) garantit que la méthode d'enregistrement du livre save() n'a pas été appelée, car le livre n'a pas été trouvé et la mise à jour n'a pas été effectuée.

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

Résumé

Mockito est une bibliothèque permettant de créer des objets simulés qui offre la possibilité de simuler le comportement des dépendances et d'isoler le code testé. Cela simplifie les tests, les rend plus rapides, plus stables et indépendants des ressources externes.

Tout était clair ?

Comment pouvons-nous l'améliorer ?

Merci pour vos commentaires !

Section 5. Chapitre 3
some-alt