Parmi les nouveautés de la version 3.2 de Spring, le framework de test (spring-test) s'est vu enrichir de nouvelles fonctionnalités permettant d'écrire plus facilement des tests sur la couche Controller (Model View Controller).
Outre la possibilité d'écrire des tests unitaires en utilisant des mocks, il est désormais possible de tester les contrôleurs comme si un conteneur web était démarré.
Les tests de contrôleurs peuvent être envisagés selon 2 approches :
- L'approche unitaire où les dépendances associées au contrôleur sont mockées par l'un de vos frameworks de mock préféré (Mockito (utilisé dans les exemples de ce post) / EasyMock / PowerMock / JMockit ou autre)
- L'approche globale où toute la chaîne d'appels est testée de bout en bout de l'application (de la couche contrôleur à la base de données)
Le point central du framework de test Spring MVC repose sur la classe MockMVC. C'est à partir d'une instance de cette classe que les tests vont pouvoir s'écrire en simulant des requêtes HTTP (POST/GET) et en vérifiant les réponses (du statut au type de contenu en passant par les redirections d'URL et le contrôle de l'affichage des vues).
Afin d'illuster les 2 approches d'écriture, je vous propose de partir d'un contrôleur MVC relativement simple affichant un message d'accueil lorsque l'utilisateur se connecte. La partie authentification est géré par le framework Spring-Security.
@RequestMapping("/") @Controller public class AccueilController { @Autowired private UtilisateurService utilisateurService; @RequestMapping(method = RequestMethod.GET) public String accueil(final Model model, final Principal principal) { final Utilisateur user = this.utilisateurService.findUtilisateurByLogin(principal.getName()); model.addAttribute("user", user); return "accueil"; } }
Le contrôleur se charge de récupérer l'objet Utilisateur associé au jeton de sécurité et de le mettre à disposition sur la vue afin d'afficher un message personnalisé à l'utilisateur (son prénom, son nom et sa date de dernière connexion).
La classe de test suivante permet de tester le traitement complet de la requête HTTP de la page d'accueil jusqu'à sa réponse en passant par l'ensemble des couches applicatives (Contrôleur / Service / DAO / Base de données)
@RunWith(SpringJUnit4ClassRunner.class) @WebAppConfiguration @ContextConfiguration(locations = {"classpath:/META-INF/applicationContext.xml", "classpath:/META-INF/webmvc-config-test.xml" }) public class AccueilControllerTest { private MockMvc mockMvc; @Autowired private WebApplicationContext wac; @Autowired private UtilisateurService utilisateurService; @Before public void setUp() { this.mockMvc = webAppContextSetup(this.wac).build(); final Authentication authentication = new TestingAuthenticationToken("celine.gilet", "netapsys"); final SecurityContext securityContext = new SecurityContextImpl(); securityContext.setAuthentication(authentication); SecurityContextHolder.setContext(securityContext); } @After public void tearDown() { SecurityContextHolder.clearContext(); } /** * Méthode en charge de tester l'affichage de la page d'accueil. * @throws Exception Exception */ @Test public void testAccueil() throws Exception { // Utilisateur attendu final Utilisateur userExpected = this.utilisateurService.findUtilisateur(1L); // Exécution de la requête HTTP this.mockMvc .perform(get("/").principal(SecurityContextHolder.getContext().getAuthentication())) // Affichage des traces .andDo(print()) // Vérification de l'objet du model passé à la vue .andExpect(model().attribute("user", userExpected)) // Vérification de l'affichage de la vue en retour .andExpect(view().name("accueil")) // Vérification du statut la response HTTP (Code 200) .andExpect(status().isOk()); } }
Il est possible de transformer le test précédent pour le rendre unitaire en utilisant des mocks avec Mockito. Le test ne passera alors plus par toutes les couches de l'application et n'ira plus jusqu'en base de données.
@RunWith(SpringJUnit4ClassRunner.class) @WebAppConfiguration @ContextConfiguration(locations ={"classpath:/META-INF/applicationContext.xml", "classpath:/META-INF/webmvc-config-test.xml" }) public class AccueilControllerMockTest { private MockMvc mockMvc; @Autowired private WebApplicationContext wac; @Mock private UtilisateurService utilisateurService; @InjectMocks private AccueilController accueilController; @Before public void setUp() { MockitoAnnotations.initMocks(this); this.mockMvc = webAppContextSetup(this.wac).build(); final Authentication authentication = new TestingAuthenticationToken("celine.gilet", "netapsys"); final SecurityContext securityContext = new SecurityContextImpl(); securityContext.setAuthentication(authentication); SecurityContextHolder.setContext(securityContext); } @After public void tearDown() { SecurityContextHolder.clearContext(); } /** * Méthode en charge de tester l'affichage de la page d'accueil. * @throws Exception Exception */ @Test public void testAccueil() throws Exception { // Utilisateur attendu final Utilisateur userExpected = new Utilisateur(); userExpected.setId(1L); Mockito.when(this.utilisateurService.findUtilisateurByLogin(SecurityContextHolder.getContext().getAuthentication().getName())) .thenReturn(userExpected); // Exécution de la requête HTTP this.mockMvc .perform(get("/").principal(SecurityContextHolder.getContext() .getAuthentication())) // Affichage des traces .andDo(print()) // Vérification de l'objet du model passé à la vue .andExpect(model().attribute("user", userExpected)) // Vérification de l'affichage de la vue en retour .andExpect(view().name("accueil")) // Vérification du statut la response HTTP (Code 200) .andExpect(status().isOk()); } }
L'affichage des traces sur l'objet MockMVC à l'aide de l'instruction andDo(print()) permet de voir en détail ce qui se passe :
MockHttpServletRequest: HTTP Method = GET Request URI = / Parameters = {} Headers = {} Handler: Type = fr.netapsys.springmvc.web.AccueilController Method = public java.lang.String fr.netapsys.springmvc.web.AccueilController.accueil(org.springframework.ui.Model,java.security.Principal) Resolved Exception: Type = null ModelAndView: View name = accueil View = null Attribute = user value = Utilisateur #1 errors = [] FlashMap: MockHttpServletResponse: Status = 200 Error message = null Headers = {} Content type = null Body = Forwarded URL = /WEB-INF/layouts/default.jspx Redirected URL = null Cookies = []
Vous pouvez retrouver l'ensemble du code de cet exemple à l'adresse suivante : springmvctest