Chaque rupture d’API qui a atteint la production dans mon équipe a passé la CI. Toutes. Les tests unitaires étaient verts. La suite d’intégration a passé. Le déploiement est sorti, et puis les messages Slack ont commencé.
Le problème n’était pas que nous ne testions pas. Nous testions la mauvaise chose. La plupart des pipelines CI vérifient que le code s’exécute. Ils ne vérifient pas que le contract API entre producteur et consommateur est encore intact.
Pourquoi vos tests existants ne détecteront pas les ruptures d’API
Les tests unitaires exercent votre logique interne. Ils appellent votre handler avec des dépendances simulées et font des assertions sur la réponse. Les tests d’intégration démarrent toute votre stack et frappent de vrais endpoints.
Les deux testent le producteur. Ils vérifient que votre API se comporte correctement quand elle est appelée comme vous l’appelez actuellement. Ils ne testent pas si un consommateur externe, construit contre le contract de la semaine dernière, peut encore communiquer avec vous.
Les ruptures subtiles sont les dangereuses. Vous renommez un champ de user_id à userId parce que votre linter s’est plaint. Vous changez une réponse 200 pour retourner un objet imbriqué au lieu d’un objet plat. Vous rendez un paramètre de requête obligatoire qui était optionnel. Vos propres tests sont mis à jour dans la même PR, donc tout passe. Mais chaque client en production casse.
C’est la différence entre tester du code et tester des contracts.
Ce qui compte comme une rupture d’API
Une rupture est toute modification qui fait échouer un client correctement implémenté. Ce n’est pas une question de bugs. C’est une question de la promesse que votre API a faite.
Les ruptures les plus courantes se répartissent en trois catégories :
- Changements structurels : Supprimer ou renommer des champs, changer des types, modifier l’imbrication
- Changements comportementaux : Rendre des paramètres optionnels obligatoires, changer les valeurs par défaut de pagination, modifier la forme des réponses d’erreur
- Changements de cycle de vie : Supprimer des endpoints, changer des chemins d’URL, déprécier des versions sans avertissement
Certains sont évidents. D’autres ne le sont que si vous regardez l’API du point de vue du consommateur. La plupart des équipes ne le font pas.
Couche 1 : Détecter les ruptures structurelles avec le diffing OpenAPI
Les specs OpenAPI décrivent la forme de votre API. Si vous traitez votre spec comme un contract, vous pouvez la comparer (diff) avec la version précédente et signaler les ruptures avant la fusion.
Des outils comme oasdiff comparent deux documents OpenAPI et catégorisent les changements comme rupture, dangereux ou sûr. Une rupture est quelque chose comme supprimer un champ de réponse ou changer un paramètre d’optionnel à obligatoire.
Voici à quoi ça ressemble dans un flux de travail GitHub Actions :
# .github/workflows/api-contract.yml
name: API Contract Check
on:
pull_request:
paths:
- 'openapi.yaml'
jobs:
diff:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Download oasdiff
run: |
curl -L https://github.com/Tufin/oasdiff/releases/latest/download/oasdiff_linux_amd64.tar.gz | tar xz
sudo mv oasdiff /usr/local/bin/
- name: Check for breaking changes
run: |
oasdiff breaking \
--base origin/main:openapi.yaml \
--revision openapi.yaml \
--fail-on WARN
Ça fait échouer le build si la PR introduit une rupture structurelle. C’est rapide, déterministe, et attrape les renommages et suppressions que les tests d’intégration manquent.
Il y a un hic. Le diffing OpenAPI ne voit que le schéma. Il ne peut pas dire si vous avez changé le sens d’un champ en gardant le même type. Un champ booléen active qui signifie maintenant « email vérifié » au lieu de « compte activé » n’apparaîtra pas dans le diff. Le type n’a pas changé. Le contract, si.
Couche 2 : Vérifier les consumer contracts avec Pact
Le consumer-driven contract testing inverse le modèle. Au lieu que le fournisseur d’API affirme sa propre correction, les consommateurs définissent ce dont ils ont besoin. Ces attentes deviennent des contracts que le fournisseur doit satisfaire en CI.
Voici comment ça marche. Votre équipe frontend écrit un test qui dit : « Quand j’appelle GET /users/123, j’attends un 200 avec un body contenant user_id comme string. » Pact enregistre cette interaction et stocke le contract.
Côté fournisseur, votre CI backend récupère tous les consumer contracts et les rejoue contre le code actuel. Si une PR supprime user_id ou le change en number, la provider verification échoue. Même si vos propres tests passent.
Un test consommateur minimal avec Pact JS :
// consumer.spec.js
const { PactV3 } = require('@pact-foundation/pact');
const { expect } = require('chai');
const provider = new PactV3({
consumer: 'web-app',
provider: 'user-service',
dir: './pacts',
});
describe('GET /users/:id', () => {
it('returns the user', async () => {
await provider
.given('user exists')
.uponReceiving('a request for user 123')
.withRequest({
method: 'GET',
path: '/users/123',
})
.willRespondWith({
status: 200,
headers: { 'Content-Type': 'application/json' },
body: {
user_id: '123',
email: 'alice@example.com',
},
});
await provider.executeTest(async (mockserver) => {
const user = await fetchUser(mockserver.url, '123');
expect(user.user_id).to.equal('123');
});
});
});
La provider verification en CI :
# .github/workflows/verify-contracts.yml
name: Verify Consumer Contracts
on: [pull_request]
jobs:
verify:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Verify pacts
run: |
docker run --rm \
-v $(pwd)/pacts:/pacts \
pactfoundation/pact-cli \
verify-provider \
--provider-app-version ${{ github.sha }} \
--pact-broker-base-url https://your-pact-broker.io \
--provider user-service
Ça attrape les ruptures sémantiques que le schema diffing ne peut pas voir. Si un consommateur compte sur un format de valeur spécifique ou un message d’erreur, Pact le signalera.
Les compromis que la plupart des équipes ignorent
Aucune des deux approches n’est gratuite.
Le diffing OpenAPI exige que vous mainteniez une spec précise. Si votre spec est générée à partir d’annotations de code, elle reste à jour automatiquement. Si elle est écrite à la main, elle va dériver, et le diff devient sans signification. Les specs générées sont meilleures pour ce flux de travail.
Pact exige une discipline organisationnelle. Les consommateurs doivent écrire des contract tests. Vous avez besoin d’un broker pour stocker et versionner les contracts. Quand l’attente d’un consommateur est fausse, quelqu’un doit négocier le changement. Ça crée de la friction. C’est le but. La friction empêche les ruptures silencieuses.
Faire tourner les deux est idéal mais pas toujours pratique. Si vous avez un ou deux consommateurs critiques, Pact est rentable rapidement. Si vous avez des dizaines de consommateurs d’API anonymes, le diffing OpenAPI est le meilleur point de départ.
Une chose de plus : aucun des deux outils ne détecte les régressions de performance ou les changements d’authentification. Un nouvel endpoint qui exige OAuth là où aucun n’était nécessaire auparavant est une rupture. Votre outil de diff pourrait ne pas le signaler si le schéma d’auth était déjà défini ailleurs. Gardez l’œil ouvert pour les failles.
FAQ
Qu’est-ce qu’une rupture d’API ?
Une rupture d’API est toute modification d’une API qui fait échouer des clients existants et correctement implémentés. Cela inclut supprimer des champs, changer des types, rendre des paramètres optionnels obligatoires, ou modifier les codes de statut de réponse pour des endpoints existants.
En quoi le API contract testing diffère-t-il du testing d’intégration ?
Les tests d’intégration vérifient que votre système fonctionne dans son ensemble. Les contract tests vérifient que l’interface publique de l’API correspond à ce que les consommateurs attendent. Les tests d’intégration peuvent passer même quand le contract casse, si les deux côtés du système sont mis à jour dans le même commit.
Puis-je utiliser le diffing OpenAPI sans écrire de specs à la main ?
Oui. Des outils comme oasdiff fonctionnent avec n’importe quel document OpenAPI, y compris ceux générés à partir d’annotations de code en utilisant des bibliothèques comme SpringDoc, FastAPI ou drf-spectacular. Les specs générées sont souvent plus fiables parce qu’elles ne peuvent pas dériver de l’implémentation.
Ai-je besoin d’un broker Pact pour utiliser les consumer-driven contracts ?
Pour les équipes avec plus de quelques services, oui. Le broker stocke les contracts, suit les versions, et montre quels consommateurs sont affectés par un changement proposé. Sans lui, vous passez des fichiers de contract manuellement, ce qui s’effondre rapidement.
Et GraphQL ?
GraphQL a une sémantique de rupture différente. Supprimer un champ est une rupture, mais en ajouter un est sûr. Des outils comme GraphQL Inspector fournissent un schema diffing similaire aux outils OpenAPI. Pact supporte aussi les interactions GraphQL.
Commencez par le diff de schéma
Commencez par le diffing OpenAPI. C’est l’effort le plus faible et ça attrape les ruptures les plus courantes. Ajoutez-le à votre CI aujourd’hui, même si votre spec est imparfaite. Une vérification imparfaite vaut mieux que pas de vérification.
Une fois que vous aurez attrapé une rupture en revue de code qui aurait gâché le week-end de quelqu’un, vous comprendrez pourquoi c’est important.