Contract Testing : le chaînon manquant entre tests d'intégration et microservices


Le scénario

Deux équipes. Deux services. Une API entre eux.

L’équipe B consomme l’API de l’équipe A pour récupérer des données utilisateur. Elle a écrit de bons tests d’intégration — avec des stubs qui simulent les réponses de A.

L’équipe A décide de renommer un champ dans sa réponse : lastName devient familyName. Migration propre, documentée, déployée.

Les tests de l’équipe B passent toujours. Les stubs n’ont pas changé — ils retournent encore l’ancien format. En production, B désérialise familyName comme null. Les bugs pleuvent.

Le problème : les stubs ne vieillissent pas avec les contrats qu’ils simulent.

Pourquoi les tests classiques ne voient pas ça

  • Les tests unitaires de B testent le code de B en isolation. Le stub retourne ce qu’on lui a dit de retourner.
  • Les tests d’intégration de B utilisent un stub de A. Si le stub n’est pas mis à jour, ils passent malgré l’incompatibilité.
  • Les tests de A ne savent pas que B dépend du format lastName.

Il n’existe pas de test qui vérifie que ce que A produit correspond à ce que B attend.

L’idée du Contract Testing

Le Contract Testing établit un contrat formel entre consommateur et fournisseur, et vérifie que les deux respectent ce contrat.

Il existe deux formes :

Consumer-Driven Contract Testing (CDC) Le consommateur (B) décrit ce qu’il attend de l’API de A. Ce contrat est soumis au fournisseur (A), qui vérifie qu’il peut le satisfaire. Si A modifie son API d’une manière qui casse le contrat, le test échoue — avant la mise en production.

Provider Contract Testing Le fournisseur publie un contrat de ce qu’il garantit. Les consommateurs vérifient que leurs usages sont couverts par ce contrat.

Un exemple avec Pact (.NET)

Pact est le framework de référence pour le Consumer-Driven Contract Testing.

Côté consommateur (équipe B) :

// Le consommateur décrit ce qu'il attend
[Fact]
public async Task GetUser_ReturnsExpectedFormat()
{
    // On définit le contrat : "quand je demande /users/1,
    // j'attends un objet avec id, firstName, lastName"
    pact
        .UponReceiving("a request for user 1")
        .WithRequest(HttpMethod.Get, "/users/1")
        .WillRespond()
        .WithStatus(200)
        .WithBody(new
        {
            id = 1,
            firstName = "Alice",
            lastName = "Dupont"   // ← le consommateur dépend de CE champ
        });

    // Le test vérifie que le client peut utiliser cette réponse
    var client = new UserApiClient(pact.MockServerUri);
    var user = await client.GetUser(1);

    Assert.Equal("Alice", user.FirstName);
    Assert.Equal("Dupont", user.LastName);
}

Ce test génère un fichier de contrat (JSON). Ce contrat est publié dans un Pact Broker.

Côté fournisseur (équipe A) :

// A vérifie que son API respecte le contrat publié par B
[Fact]
public void ProviderApi_SatisfiesConsumerContracts()
{
    var config = new PactVerifierConfig();
    var verifier = new PactVerifier(config);

    verifier
        .ServiceProvider("UserService", new Uri("http://localhost:5000"))
        .WithPactBrokerSource(new Uri("http://pact-broker"))
        .Verify();
}

Quand A renomme lastName en familyName, ce test échoue chez A — avant que le changement soit déployé. A sait qu’il casse un consommateur. A peut coordonner la migration.

Ce que ça change

Sans Contract Testing :

A modifie son API → déploie → B casse en production → équipe B cherche pourquoi

Avec Contract Testing :

A modifie son API → tests contrats échouent chez A → A coordonne avec B avant de déployer

La rupture est détectée au moment du changement, pas après le déploiement.

Limites

Le Contract Testing n’est pas gratuit :

  • Il faut un Pact Broker (service hébergé ou Pact Broker Cloud) pour partager les contrats
  • Il nécessite une coordination entre équipes — les deux sides doivent jouer le jeu
  • Il ne teste pas la logique métier, seulement la compatibilité des formats
  • Il est surtout pertinent en contexte de microservices où plusieurs équipes possèdent des services indépendants

Dans un monolithe ou une petite équipe unique, les tests d’intégration classiques avec une vraie instance du service suffisent.

En résumé

  • Les stubs utilisés dans les tests d’intégration ne vieillissent pas avec les contrats des vrais services
  • Le Contract Testing établit un contrat formel entre consommateur et fournisseur
  • Le Consumer-Driven Contract Testing (Pact) permet au consommateur de définir ses attentes, que le fournisseur vérifie avant chaque déploiement
  • Les ruptures de contrat sont détectées avant la mise en production
  • C’est un outil de microservices multi-équipes — pas une solution universelle

Si votre organisation souffre de bugs de compatibilité d’API entre services, le Contract Testing est probablement le chaînon manquant dans votre stratégie de test.


Le Contract Testing avec Pact est au programme du TP de ma formation Tests & Validation au CNAM Strasbourg.