MVC test framework dans un context securisé - abenhamda/demo GitHub Wiki
La phase de test est une étape obligatoire et importante dans le cycle de développement de logiciels quel que soit la méthodologie utilisée.
Spring fournit pour cette étape un ensemble de modules permettant de faciliter le travail de développeur pour la création de tests avec des interactions simples entre les services de Spring (Injection, Sécurité, Transaction...). Parmi ces modules on trouve le MockMvc qui permet de tester des services REST.
La façon standard pour tester un service Rest consiste à déployer ce service sous un conteneur permettant de le rendre disponible pour test (serveur dédié, mode embarqué avec plugin maven ,...), puis utiliser le RestTemplate de Spring pour simuler le client.Ce prérequis rend le test d’un service Rest plus complexe que le test d’un bean Spring standard :
- Nécessite un tiers supplémentaire (tomcat, plugin maven,...).
- Temps d’exécution plus lent (démarrage et arrêt du conteneur).
- L’exécution d’un seul cas de test nécessite le déploiement de toute l’application.
MockMvc est un module de SpringTest permettant de simplifier la création de tests Rest et retirer la dépendance explicite à un conteneur web. Une démonstration complété est disponible sous github.
Le MockMvc fournit un support JUnit pour tester un code Spring MVC d’une manière simple.- Introduit dans spring-test.jar.
- Requête traitée via DispatcherServlet.
- Pas besoin d’un WEB container pour tester.
Afin de simplifier l’écriture de nos classes de test, on peut commencer par importer statiquement trois classes clés :
/** *Contenant un ensemble de static method permettant de logger *le résultat de test (print(), log() ) **/ import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.*; /** *Contenant un ensemble de static method permettant d’accéder aux assertions de *différents éléments de la réponse (status(), header(), content(), cookie(),...) **/ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; /** *Contenant un ensemble de static method permettant de builder une requête MVC * (get(), post(), put(),...). **/ import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
Maintenant on peut configurer notre test pour que spring puisse démarrer le contexte associé :
/** *Test runner. **/ @RunWith(SpringJUnit4ClassRunner.class) @WebAppConfiguration @ContextConfiguration(classes = {RestConfigTest.class }) public final class MySimpleTest {}
Que font ces annotations:Le bean utilisé pour configurer le WebApplicationContext :
- @WebAppConfiguration : Demander au test runner d'instancier le WebAppConfiguration comme un bean Spring sous le nom 'webApplicationContext'. La présence de cette annotation garantit qu'un web context sera chargé pour le test en utilisant un chemin d’accès par défaut pour la racine de l'application web, ce chemin peut être modifié via l'attribut 'value'.
- @ContextConfiguration : Une annotation de Spring Test, permettant de configurer le contexte web de l'application (DataSource, beans, Security,...) chargée pour le test. Possible d'injecter une config xml via l'attribut 'locations'. A partir de la version 3.1 Spring introduit la possibilité d'utiliser des classes de configuration (@Configuration) via l'attribut 'classes'.
/** * Configuration de module MVC dans l'application. **/ @Configuration @EnableWebMvc @ComponentScan(basePackages = Constant.PACKAGE + ".controller") public class RestConfigTest { }Il ne reste qu’à initialiser le mockMvc, cet objet représente un point d’entrée au serveur MVC et de manipulation de différentes entrées REST de l'application. Cet objet est instancié a partir du contexte web crée précédemment par annotation (@WebAppConfiguration), du coup on aura besoin de récupérer cette instance par injection Spring:
/** *Injecter l'instance de WebApplicationContext. **/ @Autowired private WebApplicationContext webApplicationContext; /** *Sauvegarder l'objet mockMvc. **/ private MockMvc mockMvc; /** *Initialiser et builder notre mockMVC. **/ @Before public void setup(){ mockMvc= MockMvcBuilders.webAppContextSetup(webApplicationContext).build(); }
Pour plus de détail: La classe MockMvcBuilders utilisée précédemment possède deux méthodes permettant d’instancier le MockMvc:Finalement tout est prêt pour implémenter notre test:
- Une méthode permettant de builder une instance de MockMvc liée à un web context passé en paramètre. Le DispatcherServlet utilisera ce contexte pour définir l'ensemble de contrôleurs REST de l'application.
public static DefaultMockMvcBuilder webAppContextSetup(WebApplicationContext context);- Une méthode permettant de builder une instance de MockMvc en mode Standalone et enregistrer un ou plusieurs contrôleurs de l'application dans le web context par programmation. Cela est très utile pour tester une partie de l'application.
public static StandaloneMockMvcBuilder standaloneSetup(Object... controllers);
@Test public void simpleTest() throws Exception { //Lancer une requête Rest de type GET pour l'url '/data' mockMvc.perform(get("/data")) //Assert le statut de la réponse http est égal a OK. .andExpect(status().isOk()) //Assert le type de contenue de réponse. .andExpect(content().contentTypeCompatibleWith(MediaType.APPLICATION_JSON)) //Assert l’existence d'une réponse json. .andExpect(jsonPath("$").exists()) //Assert l’existence d'un attribut 'java-version' dans la réponse json. .andExpect(jsonPath("$.java-version").exists()) //Assert le type de l'attribut 'java-version' .andExpect(jsonPath("$.java-version").isString()) //Assert la valeur de l'attribut 'java-version' dans la réponse json. .andExpect(jsonPath("$.java-version").value("8.24")) //Afficher la réponse. .andDo(print()); }
Pour plus de détail:On va changer la configuration de l'application afin d’intégrer Spring Security. Dans notre exemple, chaque utilisateur de l'application est identifié par un token communiqué via le cookie au serveur. Un petit aperçu sur la configuration de sécurité de l'application (La config Spring Security n'est pas couvert par cet article).
- La méthode perform permet d'envoyer la requête (RequestBuilder ) au serveur Rest puis de retourner un objet de type ResultActions encapsulant la réponse.
- L' objet ResultActions permet d'effectuer des assertions enchaînées via la méthode andExpect(), effectuer des actions (loguer ,...) via la méthode andDo() et finalement retourner la réponse via la méthode andReturn() qui renvoi un objet MVCResult .
- L'objet MVCResult permet d’accéder directement à la réponse (contenue, statut, exceptions,...) , par contre son contenue est de type String (json, xml,..) en fonction de format de retour de service Rest, donc on peut mapper manuellement cette réponse pour obtenir un objet:
//Mapper Jackson. @Autowired private ObjectMapper jsonMapper; //Récupérer résultat sous format json. String stringResult= mockMvc.perform(get("/data")) .andReturn() .getResponse() .getContentAsString(); //Mapper manuellement le résultat vers un objet. Response objectResult= jsonMapper.readValue(stringResult, Response.class);- La méthode jsonPath permet d’accéder au contenue de la réponse json a l'aide d'une expression JsonPath afin d'effectuer des assertions sur ses attributs, une dépendance maven est indispensable pour utilisé cet api:
<dependency> <groupId>com.jayway.jsonpath</groupId> <artifactId>json-path</artifactId> <version>2.2.0</version> </dependency>Une méthode xpath est aussi disponible pour faire la même chose mais avec un contenu de la réponse en xml.
@Configuration @EnableWebSecurity @EnableGlobalMethodSecurity(prePostEnabled = true, proxyTargetClass = true) public class SecurityConfig extends WebSecurityConfigurerAdapter { @Autowired private AuthenticationFilter authenticationFilter;Le filtre d'authentification:@Override protected void configure(final HttpSecurity http) throws Exception { http.sessionManagement() .sessionCreationPolicy(SessionCreationPolicy.STATELESS); http.addFilterBefore(authenticationFilter, UsernamePasswordAuthenticationFilter.class); http.authorizeRequests() .antMatchers(Constant.ROOT_PATH, Constant.VERSION_PATH) .permitAll() .anyRequest().fullyAuthenticated(); http.csrf().disable(); }
}
@Component public class AuthenticationFilter extends GenericFilterBean { @Autowired private AuthenticationService authenticationService; public void doFilter(final ServletRequest servletRequest, final ServletResponse servletResponse, final FilterChain chain) throws IOException, ServletException { final HttpServletRequest request = (HttpServletRequest) servletRequest; final HttpServletResponse response = (HttpServletResponse) servletResponse; final String authToken = Cookies.getValue(request); UsernamePasswordAuthenticationToken auth = authenticationService.authenticate(authToken); SecurityContextHolder.getContext().setAuthentication(auth); chain.doFilter(request, response); } }
Maintenant on revient a la configuration de notre test afin d'introduire cette nouvelle fonctionnalité. Pour que le contexte (WebApplicationContext) de notre test le découvre on devrait intégrer la configuration de sécurité dans le contexte de test :
@RunWith(SpringJUnit4ClassRunner.class) @WebAppConfiguration @ContextConfiguration(classes = {RestConfigTest.class, SecurityConfig.class }) public final class MySimpleTest {}
Jusque là, le MockMvcBuilder n'est pas encore configuré pour appliquer la config de Spring Security. Pour cela une petite modification à introduire au moment de la création de MockMvc:
@Autowired private WebApplicationContext webApplicationContext; private MockMvc mockMvc; @Before public void setup(){ mockMvc= MockMvcBuilders.webAppContextSetup(webApplicationContext) //Utiliser l'object mockMvc avec un contexte sécurisé par SpringSecurity. .apply(springSecurity()).build(); }
Pour plus de détail:Lancer le test avec un utilisateur authentifié par username et password et par annotation:
- La méthode apply() permet d'appliquer une configuration (MockMvcConfigurer) spécifique pour notre environnement de test (exemple la sécurité).
- La méthode springSecurity() permet de configurer le MockMvcBuilder pour une utilisation avec Spring Security, elle crée un bean Spring sous le nom "springSecurityFilterChain" comme un filtre, et rajoute aussi un TestSecurityContextHolderPostProcessor afin de supporter l'annotation sur la méthode test @WithMockUser>qui permet de créer un utilisateur de test.
@Test @WithMockUser(username = "user", password = "pwd") public void authTest() throws Exception { mockMvc.perform(get("/data")); }Il est possible aussi de créer l'utilisateur authentifié par programmation :
@Test @WithMockUser(username = "user", password = "pwd") public void simpleTest() throws Exception { @Test public void authTest() throws Exception { mockMvc.perform( get("/data") //Créer l'utilisateur authentifié pour la requête, possible de l'attribuer //des rôles aussi. .with(user("user").password("pwd").roles("ROLE1","ROLE2")) ); } }Dans notre cas, l'utilisateur de l'application est authentifié par le biais d'un token, donc le builder de la requête permet aussi de créer des cookies :
@Test public void authTest() throws Exception { mockMvc.perform( //Créer une requête Rest de type PUT. put("/data") //Attribuer le type de contenue de la requête. .contentType(MediaType.APPLICATION_JSON) //Attribuer le contenue de la requête sous forme json. .content("{\"value\": \"My message\"}") //Attribuer le token d'authentification dans un Cookie. .cookie(new Cookie("AUTH_TOKEN", "TOKEN_SUPER_USER_VALUE"))) //Assert la réponse. .andExpect(status().isOk()) .andDo(print()); }
Le MockHttpServletRequestBuilder fournit encore d'autres moyens pour créer et authentifier un utilisateur de test :Pour conclure, le framework de Spring Test MVC est un outil complet pour la mise en place de tests unitaires et tests d’intégration pour des services Rest, et c'est un bon moyen pour que le test soit indépendant de son implémentation (déjà il n'y a aucune dépendance de code source de l'application). Du coup on peut tester nos services Spring MVC sans intégrer le framework Spring MVC lui même.
- authentication(authentication): Utiliser une authentification customisée.
- securityContext(securityContext): Utiliser un contexte de sécurité customisé.
- httpBasic("user","password"): Utiliser une authentification http basique.
- ....