Spring MVC : Comment tester vos contrôleurs ?

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)

Architecture des couches applicatives

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

Laisser un commentaire

Votre adresse e-mail ne sera pas publiée. Les champs obligatoires sont indiqués avec *

Captcha *

Ce site utilise Akismet pour réduire les indésirables. En savoir plus sur comment les données de vos commentaires sont utilisées.