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.
Inhaltsverzeichnis
- 1. Warum Contract Testing Schnittstellen zwischen Services absichert
- 2. Wie Pact technisch funktioniert: Consumer, Mock und Contract
- 3. Die Pact-Datei: der Contract als maschinenlesbarer Vertrag
- 4. Der Pact Broker: Contracts veröffentlichen und verteilen
- 5. Provider-Verifikation: der Vertrag trifft auf die echte API
- 6. Breaking Changes stoppen: Contract-Verifikation in der CI
- 7. can-i-deploy, Versionierung und Tagging von Contracts
- 8. Praxisbeispiel: Hyvä-Frontend als Consumer einer Checkout-API
- 9. Contract Testing vs. volle E2E-Tests im Vergleich
- 10. Zusammenfassung
- 11. FAQ
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.