Contract testing d'API : éviter les régressions entre équipes sans multiplier les tests E2E
Une API peut réussir tous ses tests unitaires et rester disponible, tout en cassant l'application qui l'utilise. Le problème apparaît souvent après un changement qui semblait anodin : un champ renommé, une valeur devenue obligatoire, un statut HTTP différent ou une règle de pagination modifiée.
L'équipe qui fournit l'API considère que son service fonctionne. L'équipe qui la consomme découvre pourtant une régression en intégration, parfois après le déploiement. Les tests end-to-end peuvent détecter ce type de problème, mais ils deviennent vite lents, fragiles et coûteux à maintenir lorsqu'ils doivent couvrir toutes les combinaisons entre services.
Le contract testing, ou test de contrat, traite une partie du problème à la frontière entre le consommateur et le fournisseur. Il vérifie que les attentes nécessaires à une intégration restent compatibles, sans exiger de démarrer l'ensemble du système à chaque changement.
Ce n'est pas une raison pour supprimer les tests d'intégration ou les tests métier. C'est une manière de placer un contrôle précis au bon endroit, avec un retour suffisamment rapide pour influencer le développement.
Un contrat décrit une dépendance réelle, pas une API idéale
Un contrat est l'accord observable entre deux parties : le consommateur exprime ce dont il a besoin, le fournisseur vérifie qu'il peut continuer à le servir.
Dans une intégration HTTP, le contrat peut porter sur :
- la méthode et le chemin appelés ;
- les paramètres transmis ;
- les en-têtes nécessaires ;
- le statut de réponse attendu ;
- les champs effectivement utilisés dans le corps de réponse ;
- les types et formats importants ;
- les erreurs que le consommateur sait traiter ;
- les règles de compatibilité liées à une version ou à une capacité.
Le périmètre doit rester lié à un usage réel. Si une interface web ne lit que id, name et status, son contrat n'a pas besoin de figer chaque champ interne renvoyé par l'API. Plus le contrat est précis sur des détails inutiles, plus il bloque des évolutions légitimes.
À l'inverse, un contrat trop vague ne protège rien. Vérifier seulement que la réponse est un objet JSON ne détectera pas le remplacement d'une chaîne par un nombre, la disparition d'un champ indispensable ou le changement d'une erreur en succès partiel.
Le bon niveau de détail est celui qui empêche le fournisseur de casser le comportement du consommateur, sans transformer chaque exemple en obligation permanente.
OpenAPI et Pact ne répondent pas exactement au même besoin
Deux approches sont souvent mélangées alors qu'elles partent de points différents.
OpenAPI décrit une API de manière lisible par les humains et exploitable par des outils. La description peut servir à générer de la documentation, du code client ou des scénarios de validation. Elle est particulièrement utile lorsque l'API est un produit partagé, public ou consommé par de nombreuses équipes.
Pact met en œuvre une approche de contrats pilotés par les consommateurs. Un consommateur formalise les interactions dont il dépend, puis le fournisseur vérifie que son implémentation respecte ces attentes. La documentation Pact présente le contrat comme une manière de vérifier les hypothèses du consommateur auprès du fournisseur, notamment sans construire un environnement end-to-end complet. ^1
Ces approches peuvent se compléter :
- OpenAPI sert de description de référence et de support de documentation ;
- les contrats consommateurs capturent les usages effectivement nécessaires ;
- des tests côté fournisseur détectent un écart entre le comportement réel et la description ;
- des tests de compatibilité contrôlent l'évolution avant la mise en production.
Le choix dépend de la relation entre les équipes. Une API publique avec beaucoup de consommateurs a besoin d'un contrat documentaire gouverné et d'une politique de compatibilité. Deux équipes qui livrent indépendamment un parcours métier peuvent bénéficier d'un contrat ciblé sur leurs interactions. Une bibliothèque interne appelée dans le même dépôt n'a pas forcément besoin de Pact : un test d'intégration local ou un typage partagé peut être plus simple.
Le mot important est besoin. Ajouter un outil de contrat à chaque service sans clarifier la frontière ne crée pas automatiquement de la qualité.
Le test doit commencer par l'hypothèse du consommateur
Je commence par le code qui appelle l'API, pas par la liste complète des endpoints exposés.
Pour chaque parcours important, l'équipe identifie :
- l'action utilisateur ou métier déclenchée ;
- l'appel externe nécessaire ;
- la réponse minimale utilisée ;
- les erreurs que l'application doit distinguer ;
- le comportement attendu si le fournisseur est lent ou indisponible.
Cette démarche évite de produire un contrat théorique qui ne correspond à aucune décision de l'application. Elle fait aussi apparaître les dépendances cachées : un champ affiché dans une page, une valeur utilisée pour choisir un workflow ou une erreur qui déclenche une nouvelle tentative.
Un exemple de contrat utile n'essaie pas de vérifier toute la représentation d'une commande. Il vérifie plutôt que :
GET /orders/{id}renvoie une commande identifiable ;idetstatusexistent et ont un format attendu ;- l'application peut distinguer une commande absente d'un refus d'accès ;
- un statut connu entraîne le bon parcours ;
- les champs ignorés par le consommateur restent libres d'évoluer.
Le test devient alors une documentation exécutable de l'intégration. Lorsqu'un développeur modifie le fournisseur, l'échec indique quelle attente concrète est affectée.
Distinguer une rupture réelle d'un changement compatible
Toutes les modifications d'API ne sont pas des breaking changes. La difficulté est de définir la compatibilité du point de vue du consommateur.
Les changements généralement risqués sont :
- supprimer ou renommer un champ utilisé ;
- rendre obligatoire un paramètre qui ne l'était pas ;
- changer le type ou le format d'une valeur ;
- modifier la signification d'un statut existant ;
- supprimer une valeur d'énumération encore traitée ;
- remplacer un code d'erreur par un autre comportement ;
- changer la pagination, le tri ou la nullabilité ;
- réduire les droits ou les ressources accessibles à un client.
Ajouter un champ optionnel est souvent compatible, mais pas toujours. Un consommateur fragile peut désérialiser strictement la réponse ou présenter directement un objet inattendu. Le contrat ne doit donc pas seulement appliquer une définition abstraite de la compatibilité : il doit refléter les capacités et les limites du client.
Pour les événements asynchrones, le raisonnement est similaire. Ajouter un attribut facultatif peut être acceptable, tandis que changer le sens d'un événement, réutiliser un type pour une autre transition ou supprimer une valeur peut provoquer une erreur loin du producteur.
Une règle d'équipe utile consiste à rendre explicites les changements acceptés : ajouts facultatifs, nouveaux types d'événements ignorables, comportement des champs inconnus, période de coexistence et procédure de retrait. Sans cette politique, chaque évolution devient une négociation improvisée.
Le contrat ne remplace pas la validation métier
Un test de contrat peut vérifier qu'un fournisseur renvoie status: "paid". Il ne prouve pas que le paiement a été correctement autorisé, comptabilisé et rapproché.
Il ne vérifie pas non plus :
- qu'un utilisateur ne voit que ses propres ressources ;
- qu'un calcul respecte les règles commerciales ;
- qu'une transaction est atomique ;
- qu'un délai est acceptable pour le parcours ;
- qu'un fournisseur tiers est disponible dans toutes les conditions ;
- qu'une interface reste compréhensible après la modification.
Le contrat protège la forme et une partie du comportement de l'intégration. Les tests métier, les tests d'autorisation, les tests de résilience et quelques scénarios d'intégration vérifient le reste.
La répartition que je cherche est simple :
| Niveau | Question principale | Retour attendu |
|---|---|---|
| Test unitaire | Cette fonction produit-elle le bon résultat isolément ? | Très rapide |
| Test de contrat | Le fournisseur respecte-t-il l'attente du consommateur ? | Rapide et ciblé |
| Test d'intégration | Les composants fonctionnent-ils avec leur dépendance réelle ? | Plus lent, environnement contrôlé |
| Test end-to-end | Le parcours complet fonctionne-t-il pour l'utilisateur ? | Peu nombreux, représentatifs |
| Test métier et sécurité | Les règles et les autorisations sont-elles respectées ? | Selon le risque de l'action |
Le piège est de remplacer toute la pyramide par une collection de contrats. Un contrat vert ne signifie pas que le produit est utilisable ni que la donnée est correcte.
Exécuter le contrôle au bon moment dans la CI
Un contrat n'a de valeur que s'il peut empêcher une livraison incompatible. Je l'intègre donc dans le cycle de changement, avec un résultat lisible par l'équipe.
Pour une modification du consommateur :
- les tests du consommateur produisent ou mettent à jour ses attentes ;
- le contrat est publié avec l'identifiant de version du consommateur ;
- le fournisseur est vérifié contre ce contrat ;
- la pipeline bloque si une attente existante n'est plus respectée ;
- le rapport indique l'interaction rompue et le changement responsable.
Pour une modification du fournisseur, le contrôle doit s'exécuter avant le déploiement, idéalement avec tous les contrats encore actifs. Une vérification locale peut donner un premier retour, mais elle ne doit pas être la seule source de vérité : elle risque d'ignorer un consommateur qui n'est pas présent dans le dépôt du fournisseur.
Le système doit aussi savoir si un contrat correspond à une version encore déployée. Sinon, une ancienne application oubliée peut bloquer indéfiniment les évolutions, ou l'équipe peut supprimer trop tôt une compatibilité encore nécessaire.
Je documente donc pour chaque consommateur :
- la version déployée et la version testée ;
- l'équipe responsable ;
- l'environnement concerné ;
- la date de dernière vérification ;
- les capacités réellement utilisées ;
- la procédure de retrait du contrat.
La visibilité est aussi importante que le blocage. Un échec sans explication pousse les développeurs à désactiver le test. Le rapport doit relier le contrat au consommateur, au fournisseur, à la modification et à l'action attendue.
Utiliser l'expand and contract pour faire évoluer l'API
Lorsqu'une rupture est nécessaire, je déconseille le changement brutal en une seule livraison. Le pattern expand and contract permet de faire coexister les comportements pendant une transition.
La séquence typique est la suivante :
- le fournisseur ajoute le nouveau champ ou le nouvel endpoint ;
- le consommateur apprend à l'utiliser tout en conservant l'ancien chemin ;
- les contrats vérifient que les deux versions sont servies ;
- les consommateurs migrent et la télémétrie confirme que l'ancien usage disparaît ;
- l'ancien champ ou endpoint est retiré après une décision explicite.
Cette méthode a un coût : code temporaire, double écriture éventuelle, suivi des clients et nettoyage planifié. Mais ce coût est souvent inférieur à celui d'une migration coordonnée dans l'urgence, surtout lorsque plusieurs applications ou partenaires dépendent de l'API.
Le contrat ne doit pas masquer ce coût. Il doit rendre visible la date à laquelle l'ancienne attente peut être supprimée et le propriétaire de cette décision.
Les limites des contrats générés automatiquement
Générer des tests depuis OpenAPI ou depuis des exemples accélère le démarrage, mais cela ne garantit pas la pertinence des attentes.
Un générateur peut vérifier que le payload respecte un schéma sans savoir :
- quel champ est réellement critique pour le produit ;
- si une erreur doit déclencher une reprise ou un message utilisateur ;
- si une valeur est techniquement valide mais métierement interdite ;
- si l'ordre des événements importe ;
- si une réponse trop volumineuse dégrade l'expérience ;
- si l'autorisation est correcte pour le tenant courant.
Les descriptions d'API peuvent également dériver du code réel. OpenAPI recommande une description exploitable pour la documentation, la génération de code et les outils de test, mais la présence d'un document ne prouve pas que l'implémentation et les clients sont alignés. ^2
Je considère les tests générés comme un filet complémentaire. Les scénarios qui protègent le revenu, les permissions, la facturation ou une action irréversible doivent être écrits avec l'intention métier du consommateur, puis vérifiés avec des données représentatives et non sensibles.
Contrats, sécurité et responsabilité d'accès
Un contrat peut valider le format d'une réponse alors que l'API expose la ressource du mauvais utilisateur. La compatibilité n'est pas une autorisation.
Les tests doivent inclure des identités et des périmètres représentatifs :
- un utilisateur autorisé sur sa ressource ;
- un utilisateur qui tente d'accéder à celle d'un autre tenant ;
- un rôle qui peut lire mais pas modifier ;
- une ressource inexistante ;
- un état qui ne doit pas être manipulable par le client.
OWASP identifie notamment les défauts d'autorisation au niveau des objets et des fonctions parmi les risques majeurs des API. ^3 Ces contrôles doivent rester testés côté serveur, indépendamment de ce que le client demande et de ce que le contrat décrit.
Les données de contrat doivent aussi être minimisées. Ne publiez pas de jetons, de données personnelles ou de payloads de production dans un broker de contrats ou les journaux de CI. Utilisez des identifiants synthétiques, masquez les valeurs sensibles et limitez l'accès aux rapports.
Une checklist avant d'adopter le contract testing
Avant d'ajouter un outil, je vérifie :
- Quelle dépendance et quel risque voulons-nous contrôler ?
- Quel consommateur est propriétaire de chaque attente ?
- Quels champs et erreurs sont réellement utilisés ?
- Quelle définition de compatibilité l'équipe accepte-t-elle ?
- Comment un contrat est-il versionné, publié et retiré ?
- Comment le fournisseur vérifie-t-il les consommateurs encore déployés ?
- Quelle différence faisons-nous entre contrat, test métier, sécurité et end-to-end ?
- Le résultat de la CI explique-t-il précisément la rupture ?
- Avons-nous une stratégie de transition pour les changements nécessaires ?
- Les exemples, logs et rapports excluent-ils les données sensibles ?
Si ces réponses ne sont pas claires, l'outil risque de devenir une couche de maintenance supplémentaire. Le premier livrable n'est pas un fichier de test : c'est une carte des dépendances et des attentes qui doivent rester stables.
FAQ : tests de contrat d'API
Le contract testing est-il réservé aux microservices ?
Non. Il est utile dès qu'une frontière sépare un consommateur et un fournisseur qui évoluent indépendamment : API interne, service tiers, application mobile, frontend et backend ou flux de messages. Dans un monolithe livré comme un seul bloc, des tests d'intégration locaux sont parfois plus simples.
Faut-il choisir entre OpenAPI et Pact ?
Pas nécessairement. OpenAPI décrit l'interface et peut alimenter plusieurs outils. Pact formalise les interactions attendues par des consommateurs précis. Le choix dépend de la gouvernance, du nombre de consommateurs, du mode de livraison et du niveau de contrôle voulu.
Un changement compatible peut-il casser un client ?
Oui. Un client peut dépendre d'un détail non documenté, parser strictement la réponse ou interpréter différemment une valeur pourtant valide. C'est pourquoi les contrats doivent être centrés sur les usages réels et complétés par des tests de compatibilité et de robustesse.
Combien de tests end-to-end peut-on supprimer ?
Il n'existe pas de ratio universel. Supprimez les scénarios redondants seulement lorsque les contrats, les tests métier et quelques parcours représentatifs couvrent le risque qu'ils détectaient. Les parcours critiques doivent rester testés de bout en bout, même si les intégrations intermédiaires sont déjà couvertes.
Qui doit posséder le contrat ?
Le consommateur doit posséder ses attentes, le fournisseur doit posséder leur vérification et les deux équipes doivent décider ensemble de la politique de compatibilité. Un contrat imposé par une seule équipe sans discussion devient rapidement soit trop strict, soit ignoré.
Ce que je recommande, en tant que CTO hands-on
Le contract testing devient rentable lorsque plusieurs équipes doivent livrer sans attendre une validation manuelle de tout le système. Je commence par une intégration réellement douloureuse, un parcours important et quelques attentes vérifiables. Je mesure ensuite les régressions détectées, le temps de diagnostic et le coût de maintenance avant d'étendre la pratique.
La décision n'est pas « faut-il installer Pact ? ». Elle est plutôt : quelles promesses entre nos composants doivent rester vraies, qui les vérifie, et à quel moment une rupture doit bloquer la livraison ?
Un contrat bien calibré accélère le delivery parce qu'il transforme une dépendance implicite en signal rapide. Un contrat trop large ralentit chaque évolution. Un contrat trop vague donne une fausse confiance. L'enjeu d'architecture consiste à trouver le niveau de vérité utile au produit, puis à le maintenir comme du code.
C'est le type de sujet que je traite dans une mission d'audit technique ou d'intégration d'API externe : cartographier les frontières, sécuriser les changements et donner à l'équipe des contrôles qu'elle peut réellement faire vivre.
Sources et références
Benjamin Raimond
CTO hands-on : j'architecture, je code, je structure l'équipe technique. Je transforme des objectifs business en solutions robustes et durables pour les startups et PME.
En savoir plus →