Accueil Nos publications Blog Pact .Net, Consumer Driven Contract et TestHost

Pact .Net, Consumer Driven Contract et TestHost

Si vous travaillez sur des systèmes distribués faisant intervenir plusieurs équipes distinctes vous avez très certainement rencontré un jour ou l’autre une situation similaire à celle qui suit.

Votre équipe Alpha qui est un peu en avance décide de faire un peu de refactoring et de modifier une faute qui traîne depuis des mois sur l’une de ses propriétés et donc de renommer Childrens en Children, parce oui “C’est ridicule, children c’est déjà le pluriel de child alors ça ne sert à rien de mettre un S en plus !”. Ils font les modifications adéquates, tous leurs tests sont au vert. Ils décident donc de mettre en production leur travail, fiers d’avoir enfin pu corriger cette inacceptable faute pouvant à tout moment endommager le serveur.
Pendant ce temps l’équipe Beta, elle, est plutôt à la traîne sur son sprint mais en donnant tout ce qu’ils ont pu, ses membres ont réussi à tenir l’objectif. Leurs US sont valides, tous leurs tests sont au vert, et confiants, ils valident également leur mise en production. Quelques minutes plus tard des alertes arrivent chez l’équipe Beta signalant un problème bloquant sur une partie de leur applicatif… Sous pression et sans comprendre comment ni pourquoi cette partie de l’application que personne n’a touché depuis des mois se met à poser problème, ils mettent énormément de temps à comprendre qu’ils reçoivent curieusement des appels avec “Children” et non “Childrens”…

Il existe bien des manières de tester l’interaction entre les services et celle qui nous intéresse ici est le Consumer Driven Contract via le framework Pact net, une implémentation .Net de Pact.io.

Si vous voulez les détails de son fonctionnement je vous invite à lire l’un des nombreux articles expliquant comment mettre en place de manière conventionnelle Pact .Net. Mais comme je sais que vous ne le ferez certainement pas, je vous spoile un peu dans la partie suivante.

Et avec Pact, ça se serait passé comment ?

Si on reprend notre exemple précédent, comment se serait passé cette modification de "Childrens" ? L’équipe Alpha, qui consomme un des services de l’équipe Beta émet des contrats générés par Pact indiquant comment ils appellent chaque service et comment ces services sont sensés leur répondre. Ils auraient donc généré et partagé avec l’équipe Beta un contrat spécifiant que leur nouvel appel au service utilise maintenant "Children" et non "Childrens". Durant leurs tests, l’équipe Beta aurait validé chaque contrat qui leur correspond en effectuant sur leur service les requêtes contenues dans les contrats afin de vérifier la réponse renvoyée. Ils auraient alors directement pu voir qu’un de leur consommateur avait changé la manière dont ils interagissent avec eux. Le problème aurait alors été évité.

Vous pourrez me dire

Oui c’est sympa, mais pour vérifier le contrat il faut faire de vrais appels Http, il faut que j’aie mon service accessible et que je mette des données spécifiques aux tests à l’intérieur… Puisque je fais des appels à mon propre service, n’y a-t-il pas une approche me rendant un peu plus autonome ?!

Je suis content que cela vous pose problème ! La suite de cet article vous expliquera justement comment réaliser tout cela, sans aucun appel Http réel, tout en offrant la possibilité d’injecter les données à tester directement via le test.

Implementation basique de Pact .Net

Nous partirons du principe que vous avez déjà mis en place une validation classique Consumer/Provider telle que décrite dans de nombreux exemples sur Pact net. Pour vous faire gagner du temps, vous pouvez suivre les 3 étapes suivantes :

  1. Clonez ce repo sur cet Initial commit
  2. Ouvrez une invite de cmd sur le dossier “Povider/src/Provider.Api” et lancez la commande “dotnet run” pour lancer notre Provider service.
  3. Ouvrez la solution et lancez les tests “ProviderApi.Tests.Pacts”

Le projet de test devrait tester le contrat émis par le consumer (qui pour nos tests a déjà été généré par les tests du Consumer et qui est situé à la racine dans "pacts\consumer-provider.json") en effectuant les appels à notre service lancé via notre commande "dotnet run".

Vous devriez voir les tests passer. Pour être sûr qu’on ne nous prenne pas pour des poires, allons dans le contrat "pacts\consumer-provider.json" et changeons la requête de notre consumer en remplaçant par exemple "vehiculId" par "Id" et relançons nos tests. On ne nous prend pas pour des poires, avec nos modifications la validation de notre contrat échoue bien.

Step 0 done. On attaque maintenant ce pourquoi vous lisez cet article.

Stop aux appels Http !

Comment Pact effectue ses tests ?

La validation des contrats au niveau du Provider se fait via de vraies HttpRequests pointant vers le endpoint du service à tester. Sur cette implémentation basique cet endpoint est défini dans ProviderApiTests.

Cet endpoint est utilisé lorsqu’on demande à Pact de vérifier le contrat. On lui spécifie alors le ServiceProvider et son Uri.

A partir de ce point, Pact va prendre chaque requête qu’il trouve dans le fichier de contrat, réaliser son appel sur le ServiceProvider et comparer la réponse reçue à celle spécifiée dans le contrat. Si tout est OK, le contrat est validé, sinon le test échoue.

Et si on utilisait la classe TestServer à la place ?

Il y a peu de temps, Microsoft a introduit un ASP.NET Core web server permettant de réaliser des tests, Microsoft.AspNetCore.TestHost.

ASP .NET Core includes a built-in test web host that can be used to handle HTTP requests without network overhead, meaning that you can run those tests faster when using a real web host. The test web host (TestServer) is available in a NuGet component as Microsoft.AspNetCore.TestHost. It can be added to integration test projects and used to host ASP .NET Core applications…

Ok, ça parait simple. Créons un TestServer et utilisons son adresse comme "_providerUri" pour la validation de notre Pact et le tour est joué ! Et bien pas vraiment, parce que le TestServer est un faux serveur qui est exécuté en mémoire et le seul moyen de réaliser un appel Http sur ce TestServer est de passer par un HttpClient créé via ce TestServer :

Et comme Pact réalise lui-même les appels http via son propre HttpClient, nous n’avons aucun moyen de lui faire directement appeler notre TestServer… Mais comme .Net Core est bien fait, nous avons une porte de sortie : les Middlewares !

Un Middleware ? Ouais je connais… enfin j’ai déjà entendu… Mais, tu pourrais nous rafraîchir la mémoire ?

Les Middlewares on ASP .Net Core sont des couches assemblées dans les pipelines des applications pour gérer les requêtes et les réponses. En gros, lorsque vous faites un HttpRequest, par exemple, avant d’arriver à l’adresse que vous avez spécifié, votre appel traverse un ou plusieurs Middlewares et chacun d’entre eux permet de :

  • Choisir de à quel composant passer une requête traversant le pipeline
  • Effectuer des actions avant et après avoir passé la requête au composant suivant

Pour être spécifique à notre cas, notre implémentation basique de Pact .Net utilise un Middleware appelé ProviderStateMiddleware qui se place entre Pact et notre service Provider. Ce middleware de base est utilisé pour réaliser des actions permettant de configurer l’état du provider pour les tests, comme par exemple insérer les données qui sont utilisées dans les tests afin de pouvoir les valider. Si vous êtes un peu perdu, ce schéma pourrait vous éclairer :

Schema process middleware

Ok, vous vous dites sûrement "D’accord, j’ai compris le principe, mais quel est le rapport avec notre TestServer ?!". Et bien nous allons utiliser un autre Middleware pour qu’il appelle notre TestServer au lieu de le laisser continuer et faire son propre appel Http à une quelconque adresse.

Créons notre propre Middleware : InMemoryProviderStateMiddleware

Ok c’est parti ! Créons notre classe InMemoryProviderStateMiddleware dans notre projet de test "ProviderApi.Tests.Pacts/Middlewares/InMemoryProviderStateMiddleware .cs" contenant un TestServer en propriété privée et initialisée dans le constructeur.

Nous créons dans ce constructeur un TestServer qui va faire exécuter notre ProviderApi. A partir de maintenant, nous avons un faux serveur web en mémoire faisant tourner le service que nous voulons tester. Attachons-nous maintenant à rediriger les requêtes vers ce TestServer.

Les requêtes à l’intérieur du Middleware sont propagées par la fonction Invoke, en appelant le RequestDelegate "_next"

Remarque : Dans la méthode Invoke, vous pouvez voir que dans le cas où l’adresse cible de la requête est “/provider-states”, on réalise des actions spécifique en appelant la fonction HandleProviderStatesRequest(…). Il s’agit du mécanisme permettant de gérer l’état dans lequel doit se trouver le service pour valider le test. C’est dans cette méthode qu’on va, s’il le faut, ajouter des éléments dans notre service s’ils sont requis pour les tests. Nous verrons plus de détails sur ce mécanisme dans la suite de cet article.

Nous allons donc intervenir au niveau du "await this._next(context)" en remplaçant celui-ci par l’appel à notre TestServer et replacer la réponse dans notre context afin de rendre la main à l’émetteur de la requête.

A partir de maintenant, chaque requête faite par Pact en espérant atteindre un véritable web host faisant tourner notre Provider Service sera interceptée par notre Middleware et rebasculée sur notre TestServer. Le résultat retourné par le TestServer est enregistré dans le context associé à la requête et renvoyé à son émetteur, puisqu’il n’y a pas d’appel au RequestDelegate "_next".

Le dernier step pour que cela soit possible est de configurer notre projet de test pour utiliser notre nouveau Middleware. Cela se fait au niveau du Starup via la méthode d’extension "UseMiddleware()" :

Bravo ! Vous avez maintenant un projet de test avec Pact .Net qui valide les contrats du provider sans réaliser de réel HttpRequest.

Conclusion

Nous avons vu dans ce premier article comment remplacer les HttpRequest faites par Pact par des appels à un faux serveur web spécifiquement dédié aux tests : Microsoft.AspNetCore.TestHost.TestServer. A ce stade vous êtes tout de même dans l’obligation de gérer l’état du service afin qu’il puisse valider les contrats. Par exemple, si on a des tests qui valident le fait qu’on lui retourne X véhicules, ces X véhicules doivent être insérés dans notre service, via le HandleProviderStatesRequest(…) que nous avons entr’aperçu dans cet article.

Le second article visera précisément à gérer le statut du provider via le HandleProviderStatesRequest en utilisant de l’injection de dépendance afin d’injecter un repository de test directement dans notre service lors de son démarrage dans notre TestServer !

© SOAT
Toute reproduction interdite sans autorisation de l’auteur.