Tester un controleur
Pour tester un contrôleur, nous devons valider les cas suivants:
- Mappage des requêtes HTTP
- Désérialisation et validation des inputs
- Appel à la couche service
- Sérialisation des résultats
- Gestion des erreurs.
Pour couvrir tous ces cas, il est nécessaire d'écrire un test d'intégration, d'autant plus qu'un test unitaire ne remplit pas tous les cas requis pour valider un Controller.
Test d'intégration
Spring Boot facilite le test d'un Controller avec une injection des Beans dans le contexte de l'application. De plus, cette injection ne concerne que les Beans en relation avec la couche web et permet également de ne lancer que les objets dont on a besoin et de ne pas lancer tout le contexte de l'application.
-
Spring Boot fournit @WebMvcTest pour lancer un contexte d'application qui ne contient que les Beans de la couche web utilisée par un Controller.
@SpringBootTest vs @WebMvcTest, la différence entre les deux annotations est que la première démarre le contexte de l'application Spring, alors que la seconde nous aide à restreindre uniquement la couche web est c'est ce que nous avons besoin.
On passe le nom de Controller pour @WebMvcTest pour préciser que cette est uniquement pour le Controller mentionné
-
Utiliser @MockBean pour simuler la class service, puisque nous ne voulons pas tester l'intégration entre le Controller et la couche Service mais entre le Controller et la couche HTTP.
@MockBean vs @Mock, la différence entre les deux annotations est que la première fait partie de Spring, elle est utilisée dans le cas où le contexte Spring est démarré pour les tests, ce qui est le cas pour les tests d'intégration, tandis que la seconde fait partie du Framework Mockito est conçue pour Mocker un service/dao et effectuer les tests de manière unitaire & rapide car le contexte Spring n'est pas démarré.
-
MockMvc nous permet de faire de fake requêtes http et de tester les Endpoints du Controller sans lancer un serveur HTTP.
AuthenticationControllerTest est une class de test pour le AuthenticationController
-
Mappage des requêtes HTTP
Pour vérifier qu'une requete est bien mapper à une methode dans le Controller on utilise tout simplement la méthode perform() de MockMvc.
le perform() méthode vérifie aussi le type de la méthode (Get, Post, Delete, Put ..etc) et le contenu de la requête (content-type) pour plus d'options vous pouvez consulter le Javadoc
@Test
void retrieveUserInfo_should_return_200() throws Exception {
when(servie.regiter()
mockMvc.perform(post("/api/users/current").principal(principal).
.contentType(MediaType.APPLICATION_JSON)).content()
.andDo(print())
.andExpect(status().isOk());
} -
Désérialiser et valider les inputs
Pour vérifier la désérialisation les entrées, il faut s'assurer que toutes les sources d'où proviennent les données sont vérifiées, parmi lesquelles nous trouvons @PathVariable, @RequestBody, et @RequetsParam.
RegisterController.java
@PostMapping("/forums/{forumId}/register")
UserResource register(
@PathVariable("forumId") Long forumId,
@Valid @RequestBody UserResource userResource,
@RequestParam("sendWelcomeMail") boolean sendWelcomeMail) {}
RegisterControllerTest.java
@Test
void whenValidInput_thenReturns200() throws Exception {
UserResource user = new UserResource("toto", "toto@vasco.fr");
mockMvc.perform(post("/forums/{forumId}/register", 42L)
.contentType("application/json")
.param("sendWelcomeMail", "true")
.content(objectMapper.writeValueAsString(user)))
.andExpect(status().isOk());
}Sinon, pour valider les inputs, il faut développer des tests invalide par exemple, fournir un param NULL alors qu'il obligatoire pour le bon acheminement de la requête.
@Test
void whenNullValue_thenReturns400() throws Exception {
UserResource user = new UserResource(null, "toto@vasco.fr");
mockMvc.perform(post("/forums/{forumId}/register", 42L)
...
.andExpect(status().isBadRequest());
} -
Appel à la couche service
Le mapping "/api/users/current" appelle le service accountService qui cherche le login en interrogeant la classe accountRepository.
si nous voulons être sûrs que le contrôleur et le service fonctionnent correctement ensemble, nous devons tester que les méthodes du service sont appelées avec des arguments corrects. Reprenons un exemple précédent
@Test
void retrieveUserInfo_should_return_200() throws Exception {
when(principal.getName()).thenReturn("toto"); when(userService.findUserForAuthentication(principal.getName())).thenReturn(userResponse);
verif(userSerivce, times()).findUserFor //
mockMvc.perform(post("/api/users/current").principal(principal) .contentType(MediaType.APPLICATION_JSON))
.andDo(print())
.andExpect(status().isOk());
} -
Sérialisation des résultats
Après avoir passé une requête avec des paramètres validés, on va recevoir une réponse en JSON pour la plupart des cas.
Pour valider cette réponse, on peut utiliser jsonPath() matcher qui facilite l'analyse d'un document JSON.
En utilisant le même exemple de l'authentification, si la code de réponse est 200, j'attends le corps de la requête comme suit :
{
"firstName":"toto",
"name":"toto",
"mail":"toto@ub.me",
"token":"123456-789-123456",
"lastFail":null,
"nbFail":0,
"expirationToken":1679974850627
}et pour vérifier, on peut écrire :
@Test
void retrieveUserInfo_should_return_200() throws Exception {
when(principal...
mockMvc
.perform(get("/api/users").
...
.andExpect(jsonPath("$.name").value("toto"))
.andExpect(jsonPath("$.mail").value("toto@ub.me"))
.andExpect(status().isOk());
}jouer avec jsonPath via : https://jsonpath.com/
-
Gestion des erreurs.
Pour gérer les erreurs, on va utiliser Mockito pour lancer une exception, et changer le status HTTP, comme suit :
void login_should_return_404() throws Exception {
when(principal....
mockMvc
.perform(post("/api/users/current")
...
.andExpect(status().isNotFound());
}