Dans cet article, nous allons voir comment mettre en place des tests d’intégration pour une application web ASP.NET Core.
Introduction
Les tests d’intégration d’une application web ont pour but de tester les différents composants de l’application et la bonne interaction entre eux. Contrairement aux tests unitaires, où l’on teste de façon très isolée certaines parties de l’application (par exemple, une méthode d’une classe), les tests d’intégration vont parcourir les différentes couches de l’application et détecter les possibles erreurs qui peuvent se produire.
Ces tests sont beaucoup plus proches de la réalité, et de ce que l’utilisateur final va expérimenter avec l’application. Par contre, la mise en place de ces tests est plus complexe que celle des tests unitaires, et aussi leur exécution consomme plus de temps et de ressources.
Dans cet article, on détaillera comme mettre en place ces tests pour une application ASP.NET Core très simple, et comment résoudre les problèmes les plus fréquents.
Mise en place de tests d’intégration avec ASP.NET Core
Il y a différents choix techniques pour tester une application ASP.NET Core de bout en bout. Dans cet article on propose :
- Pour la base de données, l’utilisation d’une vraie base de données dédiée aux tests. On pourrait utiliser une base de données in-memory, mais ce type de base de données n’étant pas relationnel, il ne serait pas possible de détecter certaines erreurs. On pourrait ‘mocker’ aussi toute la couche d’accès à données.
- Pour l’application ASP.NET Core, Microsoft nous propose la classe WebApplicationFactory<TEntryPoint> qui permet de créer un TestServer pour héberger l’application web à tester. Notre projet de test démarrera une instance de notre application avec cette classe, pour ensuite attaquer l’ensemble des tests.
- Pour notre projet de test, on utilisera le framework NUnit pour nous aider à la mise en place de tests. Ceci fonctionne de la même manière que pour les tests unitaires. D’autres frameworks de tests sont également disponibles.
Tests d’intégration basiques
Dans ce point on va voir comment démarrer une instance de test de notre application, lancer un request HTTP contre l’application et vérifier la réponse.
L’application à tester sera une simple application MVC Core, qui ne contient que la home page :
- Contrôleur :
- Vue cshtml :
- Layout cshtml :
Dans la même solution, on aura notre projet de test, avec la classe HomePageTest. Dans cette classe, la méthode Setup sera en charge de créer une instance de l’application à tester avec l’aide de la classe WebApplicationFactory, et d’un client HTTP pour se connecter à l’application :
Dans cette classe, on peut tester l’accès à notre home page en ajoutant cette méthode de test, qui ne vérifie que le statut code de la réponse du request HTTP :
Pour vérifier que le contenu de la page soit conforme au résultat attendu, on peut utiliser l’outil HtmlAgilityPack qui va nous permettre de parser la réponse HTML et d’accéder aux différents nodes de la page. Dans le test suivant, on vérifie que la page contient bien dans le header un node HREF qui redirige vers la home de l’application :
Par défaut, la classe WebApplicationFactory va démarrer une instance de l’application paramétrée avec les valeurs gardées dans le fichier appsettings.json du projet web. Si pour les tests on a besoin de paramétrer l’application avec d’autres valeurs, on pourrait renseigner ces valeurs lors du démarrage de l’application à l’aide de la méthode UseSetting. Par exemple, dans ce test on va modifier la valeur du paramètre Version et le test vérifiera que la balise qui affiche la version dans le footer de l’application est bien saisie :
Tests d’intégration pour une application avec authentication
Il arrive souvent que l’application à tester soit sécurisée, ce qui empêche d’accéder aux pages de l’application si l’utilisateur ne s’est pas identifié au préalable. Dans ce point, on va voir comme on peut réussir à tester une application sécurisée avec ASP.NET Core Identity.
Pour sécuriser notre application de test avec Identity, dans la classe Program.cs, on ajoute ces lignes :
Et on ajoute une nouvelle page MyAccount dont l’accès sera protégé avec l’attribut [Authorize] :
- MyAccount controlleur :
- MyAccount Index cshtml vue :
Dans le test suivant, on teste que l’accès à la page /MyAccount ne soit pas autorisé et que l’utilisateur soit renvoyé à la page de login de l’application. Dans les tests, lors de la création du client HTTP qui va attaquer l’application, on devra renseigner l’option AllowAutoRedirect = false pour ne pas suivre la redirection vers la page de login, et pouvoir tester la réponse originale du serveur :
Pour contourner le problème avec l’authentication et pouvoir tester le contenu de pages sécurisées, on propose cette solution :
- Dans le projet de test, on crée une classe mock qui sera en charge de traiter l’authentication pour les tests. Dans cette classe on pourra ‘tromper’ l’application et lui indiquer que l’authentication est toujours OK, et mocker l’identité de l’utilisateur connecté. Voici un exemple de cette classe mock :
- Lors du démarrage de l’application web à tester, on va ajouter un nouveau schéma d’authentication qui se basera sur la classe MockAuthHandler :
- Après la création du client http, on le configure pour indiquer qu’il doit envoyer toujours un header « authentication » avec le nom du schéma d’authentication qu’on a configuré dans le point précédent. Avec ceci, tous les requests de notre client de test seront authentifiées OK par la classe MockAuthHandler.
Voici le code d’un test complet qui accède à la page /MyAccount :
Tests d’intégration pour une application avec des formulaires protégés par un antiforgery token
Dans les tests d’intégration, il est possible aussi de tester le submit de formulaires qui sont protégés des attaques CSRF par un token antiforgery. Etant donné que ces tokens sont générés dynamiquement par l’application web, on devra les traiter dans nos méthodes de test.
Imaginons que notre page /MyAccount contienne un formulaire pour mettre à jour le numéro de téléphone de l’utilisateur :
Côté contrôleur, la méthode UpdatePhoneNumber est protégée par un attribut de vérification de l’antiforgery token :
Donc, pour pouvoir tester la mise à jour du numéro de téléphone et passer la vérification antiforgery, le test devra envoyer à l’application web un request de type POST avec ceci :
- Le cookie antiforgery que l’application web a renvoyé lors de la réponse GET qui contient le formulaire à saisir.
- Dans le payload du request, le contenu du formulaire devra contenir un champ avec l’antiforgery token renvoyé par l’application dans le formulaire initiale.
Pour mettre en place le test :
- On crée une classe static AntiForgeryTokenExtractor qui nous aidera à traiter tout ce qui concerne la gestion antiforgery. Cette classe contient :
- Les noms du cookie et du token antiforgery dans des variables statiques.
- La méthode ExtractAntiForgeryCookieValueFrom méthode qui va extraire de la réponse GET du formulaire le cookie antiforgery.
- La méthode ExtractAntiForgeryToken qui va extraire la valeur du champ hidden antiforgery token dedans le formulaire HTML.
- La méthode ExtractAntiForgeryValues qui va extraire d’un coup tous les informations precedentes à partir de la réponse GET du formulaire.
- Lors du démarrage de l’application web à tester, dans la méthode ConfigureTestsServices, on devra configurer les noms du cookie et du token antiforgery. On utilisera ceux qui sont indiqués dans les variables statiques de la classe AntiForgeryTokenExtractor.
- Le client HTTP qui envoie le request POST pour tester le formulaire devra inclure :
- Le cookie antiforgery
- La valeur du token Antiforgery extrait du formulaire HTML.
Voici l’implémentation complète de notre test de formulaire :
Conclusion
Si les tests d’intégration sont plus difficiles à mettre en place, on voit bien que ces difficultés techniques peuvent être contournées avec différentes solutions. Résultat : on va pouvoir tester de bout en bout notre application, et mettre en place des tests plus rassurants que ceux qui testent seulement certaines parties de l’application.