Contract Testing mit Pact: Schnittstellen absichern
PASS
expect()
Testing · Contract Testing · Pact · CI/CD
Contract Testing mit Pact: Schnittstellen absichern
Breaking Changes stoppen, bevor sie die Produktion erreichen

Contract Testing mit Pact ersetzt teure End to End Tests über Servicegrenzen hinweg durch schnelle, isolierte Prüfungen. Der Consumer definiert seine Erwartungen an eine Schnittstelle als maschinenlesbaren Vertrag, den der Provider automatisiert in der eigenen CI Pipeline verifiziert. So werden inkompatible API Änderungen zwischen Frontend und Backend erkannt, lange bevor sie in Produktion Kunden betreffen.

17 Min. Lesezeit Consumer-Driven Contracts · Pact Broker · can-i-deploy Magento · Hyvä · REST & GraphQL APIs

1. Warum Contract Testing Schnittstellen zwischen Services absichert

Consumer-Driven Contract Testing dreht die klassische Testreihenfolge um. Statt dass ein zentrales Team eine vollständige End-to-End-Suite über alle beteiligten Services schreibt, formuliert der Consumer selbst, meist ein Frontend oder ein aufrufender Service, seine konkreten Erwartungen an eine Schnittstelle: welche Endpunkte er aufruft, welche Felder er aus der Antwort liest und in welcher Form er sie erwartet. Diese Erwartungen werden als Contract festgehalten, einer präzisen, maschinenlesbaren Beschreibung realer Interaktionen statt einer abstrakten Schnittstellendokumentation.

Der Nutzen zeigt sich vor allem in Microservice-Landschaften mit vielen unabhängig deploybaren Services. Volle E2E-Tests über mehrere Servicegrenzen sind langsam, brauchen aufwendige Testumgebungen mit realistischen Daten und sind anfällig für Flakiness durch Netzwerklatenz, Timing und Nebenläufigkeit. Pact als führendes Contract-Testing-Framework verlagert die Prüfung an die Grenze der Verantwortung: Der Consumer testet gegen einen Mock, der Provider verifiziert gegen den echten Contract, beide unabhängig voneinander, ohne dass jemals beide Systeme gemeinsam hochgefahren werden müssen.

2. Wie Pact technisch funktioniert: Consumer, Mock und Contract

Der Ablauf beginnt beim Consumer. In einem normalen Testframework wie Jest, Mocha oder PHPUnit definiert der Entwickler eine Interaction: einen erwarteten Request an den Provider und die erwartete Response, inklusive Status-Code, Headern und Body. Pact startet dafür lokal einen Mock-Provider-Server, gegen den der eigentliche API-Client des Consumers getestet wird. Schlägt der Test fehl, weil der Client anders formatiert oder andere Felder erwartet, ist das ein Fehler im Consumer-Code, nicht im Provider.

Entscheidend ist die Verwendung von Matchern statt hartkodierter Werte. Statt exakt "4711" zu erwarten, beschreibt like('4711'), dass ein String derselben Form erwartet wird, unabhängig vom konkreten Wert. Das hält den Contract stabil gegenüber legitimen Datenänderungen und verhindert gleichzeitig, dass Struktur-Änderungen unbemerkt durchrutschen. Nach erfolgreichem Testlauf schreibt Pact automatisch eine JSON-Datei, den eigentlichen Contract, der exakt dokumentiert, was der Consumer vom Provider erwartet.


// consumer/pricing.pact.test.js
const { PactV3, MatchersV3 } = require('@pact-foundation/pact');
const { like, eachLike, decimal } = MatchersV3;
const { fetchCartPrice } = require('../src/api/pricingClient');

const provider = new PactV3({
  consumer: 'hyva-checkout-frontend',
  provider: 'pricing-service',
  dir: './pacts',
});

describe('Pricing API contract', () => {
  it('returns the price for a valid cart id', async () => {
    provider
      .given('cart 4711 exists with two items')
      .uponReceiving('a request for the cart price')
      .withRequest({
        method: 'GET',
        path: '/v1/carts/4711/price',
        headers: { Accept: 'application/json' },
      })
      .willRespondWith({
        status: 200,
        headers: { 'Content-Type': 'application/json' },
        body: {
          cartId: like('4711'),
          currency: like('EUR'),
          grandTotal: decimal(89.97),
          items: eachLike({
            sku: like('MS-1001'),
            rowTotal: decimal(29.99),
          }),
        },
      });

    await provider.executeTest(async (mockServer) => {
      const price = await fetchCartPrice(mockServer.url, '4711');
      expect(price.currency).toBe('EUR');
      expect(price.items.length).toBeGreaterThan(0);
    });
  });
});

3. Die Pact-Datei: der Contract als maschinenlesbarer Vertrag

Die generierte Pact-Datei ist reines JSON und folgt der Pact-Spezifikation. Sie enthält Consumer- und Provider-Namen, eine Liste von interactions mit jeweiligem Request, erwarteter Response und den matchingRules, die festlegen, welche Felder exakt und welche nur strukturell geprüft werden. Genau diese Trennung zwischen konkretem Beispielwert und abstrakter Regel unterscheidet einen Pact-Contract von einem statischen OpenAPI-Schema: Der Contract beschreibt echte, ausführbare Interaktionen, keine reine Typdefinition.

Weil die Datei aus echten Testläufen entsteht, kann sie nicht veralten, ohne dass der zugehörige Consumer-Test fehlschlägt. Ändert sich das erwartete Verhalten des Frontends, ändert sich automatisch auch der Contract beim nächsten Testlauf. Das unterscheidet Pact fundamental von handgepflegter API-Dokumentation, die regelmäßig von der tatsächlichen Implementierung abweicht, weil niemand sie synchron hält.


{
  "consumer": { "name": "hyva-checkout-frontend" },
  "provider": { "name": "pricing-service" },
  "interactions": [
    {
      "description": "a request for the cart price",
      "providerState": "cart 4711 exists with two items",
      "request": {
        "method": "GET",
        "path": "/v1/carts/4711/price",
        "headers": { "Accept": "application/json" }
      },
      "response": {
        "status": 200,
        "headers": { "Content-Type": "application/json" },
        "body": {
          "cartId": "4711",
          "currency": "EUR",
          "grandTotal": 89.97,
          "items": [
            { "sku": "MS-1001", "rowTotal": 29.99 }
          ]
        },
        "matchingRules": {
          "body": {
            "$.cartId": { "matchers": [{ "match": "type" }] },
            "$.grandTotal": { "matchers": [{ "match": "decimal" }] },
            "$.items": { "matchers": [{ "match": "type", "min": 1 }] }
          }
        }
      }
    }
  ],
  "metadata": {
    "pactSpecification": { "version": "3.0.0" }
  }
}

4. Der Pact Broker: Contracts veröffentlichen und verteilen

Ein Contract, der nur lokal im Consumer-Repository liegt, nützt dem Provider nichts. Der Pact Broker ist die zentrale Anlaufstelle, an die Consumer ihre generierten Contracts nach jedem erfolgreichen Testlauf publizieren. Der Broker versioniert jeden Contract, verknüpft ihn mit der Consumer-Applikationsversion, meist dem Git-SHA, und stellt eine Netzwerkgrafik bereit, die sichtbar macht, welcher Consumer mit welchem Provider über welche Contracts verbunden ist. Das allein ist in gewachsenen Microservice-Landschaften ein erheblicher Mehrwert gegenüber verstreuter, veralteter Dokumentation.

Der Broker unterstützt außerdem Webhooks: Wird ein neuer oder geänderter Contract veröffentlicht, kann automatisch ein Build in der Provider-CI-Pipeline ausgelöst werden, der genau diesen Contract sofort verifiziert. Damit schließt sich der Kreis zwischen Consumer- und Provider-Team, ohne dass beide Teams manuell koordinieren müssen, wann eine Schnittstellenänderung geprüft wird. In der Praxis läuft das Publizieren als letzter Schritt der Consumer-Pipeline, direkt nach den bestandenen Contract-Tests.


#!/usr/bin/env bash
# Publish the generated pact file to the Pact Broker after consumer tests pass
set -euo pipefail

pact-broker publish ./pacts \
  --consumer-app-version="$(git rev-parse --short HEAD)" \
  --branch="$(git rev-parse --abbrev-ref HEAD)" \
  --broker-base-url="https://pact-broker.mironsoft.de" \
  --broker-token="$PACT_BROKER_TOKEN"

echo "Contract published for hyva-checkout-frontend, triggering provider verification via webhook"

5. Provider-Verifikation: der Vertrag trifft auf die echte API

In der Provider-Verifikation lädt der Provider die zuletzt veröffentlichten Contracts vom Broker herunter und spielt jede darin enthaltene Interaction gegen die tatsächlich laufende API ab, nicht gegen einen Mock. Die reale Response wird gegen die im Contract festgelegten Matcher geprüft. Weicht ein Feldname, ein Datentyp oder ein Status-Code ab, schlägt die Verifikation fehl, genau an der Stelle, an der die Inkompatibilität entsteht, nicht irgendwo tief in einer verschachtelten End-to-End-Testkette.

Damit Interactions wie „cart 4711 exists with two items“ überhaupt reproduzierbar getestet werden können, registriert der Provider State Handler: kleine Funktionen, die vor der jeweiligen Interaction die passenden Testdaten anlegen. Ohne State Handler müsste jeder Contract sich auf zufällig vorhandene Produktionsdaten verlassen, was die Verifikation unzuverlässig machen würde. Nach dem Lauf publiziert der Verifier das Ergebnis, erfolgreich oder fehlgeschlagen, zurück an den Broker, sodass beide Teams denselben Status sehen.


// provider/verify.ts
import { Verifier } from '@pact-foundation/pact';

async function runVerification(): Promise<void> {
  const opts = {
    provider: 'pricing-service',
    providerBaseUrl: 'http://localhost:8080',
    pactBrokerUrl: 'https://pact-broker.mironsoft.de',
    pactBrokerToken: process.env.PACT_BROKER_TOKEN,
    publishVerificationResult: true,
    providerVersion: process.env.GIT_SHA,
    consumerVersionSelectors: [
      { mainBranch: true },
      { deployedOrReleased: true },
    ],
    stateHandlers: {
      'cart 4711 exists with two items': async () => {
        await seedTestCart('4711', [
          { sku: 'MS-1001', qty: 2, price: 29.99 },
        ]);
      },
    },
  };

  await new Verifier(opts).verifyProvider();
}

runVerification().catch((err) => {
  console.error('Provider verification failed', err);
  process.exit(1);
});

6. Breaking Changes stoppen: Contract-Verifikation in der CI

Der eigentliche Wert von Contract Testing entsteht erst, wenn die Provider-Verifikation als Pflichtschritt in der CI-Pipeline verankert ist. Ändert ein Backend-Entwickler ein Antwortformat, benennt ein Feld um oder entfernt eine Property, die ein Consumer nutzt, schlägt der Verifikationsschritt fehl, und die Pipeline wird rot, bevor der Merge oder das Deployment stattfindet. Die Fehlermeldung zeigt exakt, welche Interaction und welches Feld betroffen sind, was die Fehlersuche gegenüber einem fehlgeschlagenen End-to-End-Test in einer Staging-Umgebung erheblich beschleunigt.

Ein typisches Beispiel aus der Praxis: Ein Preisdienst ändert grandTotal von einer Zahl zu einem String, weil eine neue Bibliothek Geldbeträge anders serialisiert. Ohne Contract Testing würde das erst auffallen, wenn Kunden im Checkout falsche oder gar keine Preise sehen. Mit Pact schlägt die Provider-Verifikation sofort fehl, weil der Matcher decimal einen numerischen Typ erwartet. Der Entwickler erfährt das Problem innerhalb von Sekunden, direkt im eigenen CI-Lauf, ohne dass der Consumer aktiv etwas tun muss.

7. can-i-deploy, Versionierung und Tagging von Contracts

Selbst wenn Contract und Provider verifiziert sind, bleibt eine Frage offen: Passt die Version, die gerade deployt werden soll, zu allen Versionen, die aktuell in der Zielumgebung laufen? Genau das beantwortet der CLI-Befehl can-i-deploy. Er fragt den Broker, ob eine bestimmte Provider- oder Consumer-Version bereits erfolgreich gegen alle relevanten Gegenstücke verifiziert wurde, die in der Zielumgebung, etwa „production“, aktiv sind. Ist das nicht der Fall, bricht der Deployment-Schritt kontrolliert ab, statt eine inkompatible Kombination live zu bringen.

Dafür müssen Versionen konsequent getaggt werden: Jede erfolgreiche Verifikation wird mit der Applikationsversion verknüpft, und jedes Deployment wird per record-deployment im Broker mit einem Environment-Tag wie „staging“ oder „production“ vermerkt. So entsteht eine vollständige Kompatibilitätsmatrix über alle Umgebungen hinweg, die can-i-deploy vor jedem Deployment automatisiert auswertet, statt sich auf manuelle Absprachen zwischen Teams zu verlassen.


# .github/workflows/deploy-pricing-service.yml
name: Deploy pricing-service
on:
  push:
    branches: [main]

jobs:
  verify-and-deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Run provider verification against latest contracts
        run: npm run pact:verify
        env:
          PACT_BROKER_TOKEN: ${{ secrets.PACT_BROKER_TOKEN }}

      - name: Check deployment safety
        run: |
          pact-broker can-i-deploy \
            --pacticipant pricing-service \
            --version "$(git rev-parse --short HEAD)" \
            --to-environment production \
            --broker-base-url https://pact-broker.mironsoft.de \
            --broker-token "$PACT_BROKER_TOKEN"

      - name: Deploy to production
        if: success()
        run: ./deploy.sh production

      - name: Record deployment in Pact Broker
        if: success()
        run: |
          pact-broker record-deployment \
            --pacticipant pricing-service \
            --version "$(git rev-parse --short HEAD)" \
            --environment production

8. Praxisbeispiel: Hyvä-Frontend als Consumer einer Checkout-API

In einem Magento-Setup mit Hyvä Theme rendert eine Alpine.js-Komponente im Checkout den Gesamtpreis, Steuern und aktive Rabatte, meist über die Magento-GraphQL-API oder einen dedizierten Pricing-Microservice, der neben Magento betrieben wird. Ohne Contract Testing würde man diesen Zusammenhang typischerweise mit einem Cypress-E2E-Test absichern, der eine komplette Magento-Instanz mit Testdaten hochfährt, ein Produkt in den Warenkorb legt und den gerenderten Preis im DOM prüft. Das funktioniert, ist aber langsam und bei jeder Backend-Änderung ein potenzieller Single Point of Failure für die gesamte Frontend-Pipeline.

Mit Pact schreibt das Frontend-Team stattdessen einen Consumer-Test, der exakt festlegt, welche Felder aus der Preisantwort erwartet werden, inklusive Währungsformat und Rabattstruktur. Dieser Test läuft in Millisekunden, ohne Magento, ohne Datenbank, ohne Netzwerk. Das Backend-Team verifiziert denselben Contract gegen die echte Pricing-Logik in der eigenen Pipeline, mit realistischen State Handlern für Rabattaktionen oder Steuerklassen. Beide Teams arbeiten unabhängig, der Contract ist die einzige geteilte Wahrheit zwischen Frontend und Backend.

9. Contract Testing vs. volle E2E-Tests im Vergleich

Contract Testing ist kein Ersatz für jede Form von End-to-End-Test, sondern das richtige Werkzeug für eine bestimmte Klasse von Problemen: viele unabhängige Services, teure oder flakige Cross-Service-E2E-Suiten und der Bedarf an schnellem Feedback direkt im Entwickler-Workflow. Für kritische User Journeys, sichtbares UI-Verhalten und Timing- oder Race-Condition-Probleme, die sich nur im Zusammenspiel echter Systeme zeigen, bleibt ein echter E2E- oder Integrationstest mit Cypress oder Playwright weiterhin notwendig, Contract Testing prüft diese Ebene bewusst nicht.

Dimension Contract Testing (Pact) Volle E2E-Tests über Service-Grenzen Praxis-Implikation
Geschwindigkeit Millisekunden pro Interaction, kein echtes Deployment nötig Sekunden bis Minuten pro Szenario, reale Systeme müssen laufen Contract-Tests laufen bei jedem Commit, E2E eher stündlich oder täglich
Flakiness Deterministisch, kein Netzwerk, keine Timing-Abhängigkeit Anfällig für Netzwerklatenz, Nebenläufigkeit und Testdaten-Drift Weniger rote Pipelines durch Umgebungsprobleme statt echter Bugs
Fehlerlokalisierung Exakte Interaction und Feld werden benannt Fehler kann irgendwo im gesamten Stack liegen Deutlich kürzere Diagnosezeit bei Contract-Verletzungen
CI-Kosten Kein gemeinsames Testcluster, kein Seed für mehrere Services Aufwendige Testumgebungen mit koordinierten Testdaten Contract Testing skaliert linear mit der Anzahl der Services
Abdeckung von UI-Verhalten Prüft keine Rendering-, Timing- oder Race-Condition-Effekte Einzige Methode, echte Nutzer-Journeys End-to-End zu verifizieren Kritische Checkout-Flows brauchen weiterhin echte E2E-Tests

Mironsoft

API-Testing, CI/CD-Pipelines und Qualitätssicherung für Magento- und Hyvä-Shops

Contract Testing für eure Schnittstellen einführen?

Wir analysieren eure Service-Landschaft, identifizieren die Schnittstellen mit dem größten E2E-Risiko und führen Pact mit Broker, CI-Integration und can-i-deploy-Gates produktionsreif ein, abgestimmt auf Magento, Hyvä und angebundene Microservices.

Contract-Testing-Setup

Pact-Integration für Consumer und Provider, inklusive Matcher-Strategie

Pact Broker & CI-Gates

Broker-Betrieb, Webhooks und can-i-deploy in bestehende Pipelines integrieren

E2E-Strategie

Sinnvolle Balance aus Contract-Tests und Cypress/Playwright-E2E-Suiten

10. Zusammenfassung

Contract Testing mit Pact löst ein konkretes Problem der Microservice- und API-getriebenen Entwicklung: Schnittstellen zwischen unabhängig deploybaren Teams zuverlässig abzusichern, ohne bei jeder Änderung eine vollständige, langsame End-to-End-Suite durchlaufen zu müssen. Der Consumer definiert seine Erwartungen als Contract gegen einen Mock, der Provider verifiziert denselben Contract gegen seine echte Implementierung, und der Pact Broker verbindet beide Seiten über Versionierung, Tagging und can-i-deploy-Checks zu einem geschlossenen, automatisierten Workflow.

Entscheidend ist, Contract Testing als Ergänzung zu verstehen, nicht als Ersatz. Kritische Nutzer-Journeys, sichtbares UI-Verhalten und Timing-Probleme lassen sich nur mit echten E2E-Tests wie Cypress oder Playwright abdecken. Die Stärke von Pact liegt darin, genau die Klasse von Fehlern, inkompatible API-Änderungen, früh, günstig und deterministisch im eigenen CI-Lauf zu finden, bevor sie in einer teuren, flakigen Cross-Service-Pipeline oder schlimmer, in Produktion, auffallen.

Contract Testing mit Pact - Das Wichtigste auf einen Blick

Consumer-Driven Contracts

Der Consumer definiert seine Erwartungen gegen einen Mock-Provider und erzeugt daraus automatisch die Pact-Datei.

Provider-Verifikation als CI-Gate

Der Provider spielt jede Interaction gegen die echte API ab. Abweichungen lassen die Pipeline vor dem Deploy fehlschlagen.

Pact Broker & can-i-deploy

Zentrale Versionierung, Tagging pro Umgebung und automatisierte Kompatibilitätsprüfung vor jedem Deployment.

Ergänzung, kein Ersatz für E2E

Kritische User Journeys, UI-Verhalten und Race Conditions brauchen weiterhin echte Cypress- oder Playwright-Tests.

11. FAQ: Contract Testing mit Pact

1Was ist Consumer-Driven Contract Testing?
Der Consumer hält seine Erwartungen an eine API als maschinenlesbaren Contract fest. Der Provider verifiziert diesen Contract später gegen seine echte Implementierung, statt beide Seiten in einer vollen E2E-Umgebung zu testen.
2Pact-Contract vs. OpenAPI-Schema?
OpenAPI beschreibt abstrakt und oft veraltet mögliche Endpunkte. Pact-Contracts entstehen automatisch aus echten Tests und dokumentieren reale Interaktionen mit Beispielwerten und Matching-Regeln.
3Muss der Provider Pact aktiv nutzen?
Ja. Der Provider muss den Contract mit dem Pact-Verifier gegen seine echte API prüfen. Ohne Provider-Verifikation bleibt der Contract nur eine unbestätigte Erwartung.
4Was macht der Pact Broker konkret?
Speichert Contracts versioniert, zeigt Consumer-Provider-Beziehungen, löst Verifikationen per Webhook aus und liefert die Datenbasis für can-i-deploy.
5Was prüft can-i-deploy?
Ob eine Service-Version bereits erfolgreich gegen alle in der Zielumgebung laufenden Versionen verifiziert wurde. Unverifizierte Kombinationen blockieren das Deployment.
6Wie läuft die automatische Verifikation in der CI?
Ein Webhook löst nach der Veröffentlichung einen Provider-Build aus, der den Contract lädt, jede Interaction gegen die echte API abspielt und das Ergebnis an den Broker meldet.
7Was sind State Handlers?
Funktionen im Provider, die vor einer Interaction passende Testdaten anlegen. Ohne sie wäre die Verifikation von zufällig vorhandenen Daten abhängig und nicht reproduzierbar.
8Ersetzt Contract Testing E2E-Tests komplett?
Nein. Es prüft Struktur und Format von API-Interaktionen, nicht UI-Verhalten oder Timing. Kritische User Journeys brauchen weiterhin echte E2E-Tests mit Cypress oder Playwright.
9Funktioniert Pact auch mit GraphQL?
Ja. Der Consumer-Test definiert Query oder Mutation als Request und die erwartete Datenform als Response, genau wie bei REST, und macht sichtbar, welche Felder tatsächlich genutzt werden.
10Lohnt sich Pact für kleine Teams?
Bei nur einem Consumer und Provider ist der Mehrwert gering. Sobald mehrere Services dieselbe API nutzen oder E2E-Tests langsam und flakig werden, zahlt sich der Aufwand schnell aus.