mit Pact.js Breaking Changes erkennen, bevor sie live gehen
Mocks sagen nur, ob das Frontend mit einer angenommenen API-Antwort korrekt umgeht, nicht ob diese Annahme noch stimmt. Contract Testing mit Pact.js schließt genau diese Lücke: Das Frontend definiert seine Erwartungen als ausführbaren Vertrag, und das Backend verifiziert automatisch, ob es diesen Vertrag tatsächlich erfüllt.
Inhaltsverzeichnis
- 1. Was Contract Testing wirklich löst
- 2. Das Konzept: Consumer Driven Contracts
- 3. Den Consumer-Test auf Frontend-Seite schreiben
- 4. Matcher statt exakter Werte: flexible Verträge
- 5. Provider-Verifikation auf Backend-Seite
- 6. Pact Broker: Verträge zentral austauschen
- 7. can-i-deploy: Deployments sicher absichern
- 8. Grenzen: Wann Contract Testing nicht ausreicht
- 9. Contract Testing im Vergleich zu Alternativen
- 10. Zusammenfassung
- 11. FAQ
1. Was Contract Testing wirklich löst
Ein Frontend-Team mockt die API mit MSW oder ähnlichen Werkzeugen und testet damit zuverlässig, wie die Anwendung auf eine angenommene Antwort reagiert. Das Problem: Diese Annahme kann veralten, ohne dass jemand es merkt. Ändert das Backend-Team ein Feld von price zu priceAmount, ohne das Frontend zu informieren, bleiben alle gemockten Frontend-Tests grün, während die Produktionsanwendung beim nächsten Deployment bricht. Genau dieses Szenario, verteilte Teams mit unabhängigen Deployment-Zyklen und impliziten Annahmen über eine gemeinsame Schnittstelle, ist der Ausgangspunkt für Contract Testing.
Contract Testing zwischen Frontend und Backend macht die Annahmen des Frontends über die API explizit und maschinell überprüfbar. Statt eines stillschweigenden Vertrags, der nur in Köpfen und veralteter Dokumentation existiert, entsteht ein ausführbarer Vertrag, der sowohl vom Frontend als auch vom Backend automatisiert geprüft wird. Bricht das Backend eine Erwartung, schlägt die Provider-Verifikation in der CI-Pipeline fehl, lange bevor der inkompatible Code deployt wird.
Pact.js ist die verbreitetste Implementierung für Contract Testing im JavaScript-Ökosystem, unabhängig davon, in welcher Sprache das Backend geschrieben ist. Ein Node.js-Backend, ein PHP-Backend oder ein Java-Backend lassen sich alle gegen denselben, vom JavaScript-Frontend generierten Pact-Vertrag verifizieren, weil das Pact-Dateiformat sprachagnostisch ist.
2. Das Konzept: Consumer Driven Contracts
Der zentrale Begriff bei Contract Testing ist Consumer Driven Contract. Der Consumer, in der Regel das Frontend, definiert, welche Felder und Strukturen es von einer bestimmten API-Antwort erwartet. Diese Erwartung wird bei der Ausführung des Consumer-Tests automatisch als Pact-Datei, ein JSON-Dokument mit Request- und Response-Schema, gespeichert. Der Provider, in der Regel das Backend, liest diese Pact-Datei anschließend und prüft, ob seine echte Implementierung tatsächlich eine Antwort liefert, die zu diesem Vertrag passt.
Diese Richtung, vom Consumer zum Provider, unterscheidet Contract Testing fundamental von einer klassischen OpenAPI-Spezifikation. Eine OpenAPI-Spezifikation beschreibt, was der Provider theoretisch anbietet, unabhängig davon, was einzelne Consumer tatsächlich nutzen. Ein Consumer Driven Contract beschreibt dagegen exakt das, was ein konkreter Consumer tatsächlich braucht, nicht mehr und nicht weniger. Ändert der Provider ein Feld, das kein Consumer nutzt, bricht kein Vertrag. Ändert er ein Feld, auf das ein Consumer angewiesen ist, schlägt die Provider-Verifikation gezielt fehl.
3. Den Consumer-Test auf Frontend-Seite schreiben
Der Consumer-Test läuft gegen einen von Pact.js bereitgestellten Mock-Server, nicht gegen das echte Backend. Innerhalb des Tests wird zunächst die Erwartung formuliert, welcher Request an welchen Pfad geschickt wird und welche Response dafür zurückkommen soll. Anschließend ruft der tatsächliche Frontend-Code, etwa eine Funktion, die fetch nutzt, genau diesen Mock-Server auf, wodurch der reale Code-Pfad getestet und gleichzeitig der Vertrag aufgezeichnet wird.
// npm install --save-dev @pact-foundation/pact vitest
// tests/contracts/product-api.pact.test.js
import { PactV3, MatchersV3 } from '@pact-foundation/pact';
import { describe, it, expect } from 'vitest';
import { fetchProduct } from '../../src/api/products.js';
const { like, integer } = MatchersV3;
const provider = new PactV3({
consumer: 'storefront-frontend',
provider: 'catalog-backend',
});
describe('Product API contract', () => {
it('fetches a product by id with the expected shape', () => {
provider
.given('a product with id 42 exists')
.uponReceiving('a request for product 42')
.withRequest({
method: 'GET',
path: '/api/products/42',
})
.willRespondWith({
status: 200,
headers: { 'Content-Type': 'application/json' },
body: {
id: integer(42),
name: like('Wireless Keyboard'),
price: like(49.99),
inStock: like(true),
},
});
return provider.executeTest(async (mockServer) => {
// Real application code, calling the Pact mock server instead of the real backend
const product = await fetchProduct(mockServer.url, 42);
expect(product.name).toBe('Wireless Keyboard');
expect(product.price).toBe(49.99);
});
});
});
// Running this test generates a pact file:
// pacts/storefront-frontend-catalog-backend.json
Bemerkenswert an diesem Contract Testing Ansatz ist, dass die getestete Funktion fetchProduct exakt dieselbe Implementierung ist, die auch in Produktion läuft. Der Pact-Mock-Server ersetzt lediglich das echte Netzwerk-Ziel während der Testlaufzeit. Nach erfolgreichem Testlauf existiert eine Pact-Datei, die maschinenlesbar beschreibt, was das Frontend von diesem Endpunkt erwartet, inklusive Statuscode, Headers und Body-Struktur.
4. Matcher statt exakter Werte: flexible Verträge
Ein häufiger Anfängerfehler bei Contract Testing ist, exakte Werte statt Matcher zu verwenden. Würde die Erwartung price: 49.99 statt price: like(49.99) lauten, würde die Provider-Verifikation fehlschlagen, sobald der reale Produktpreis in der Testdatenbank des Backends einen anderen Wert hat, obwohl die Struktur der Antwort völlig korrekt ist. Matcher wie like() prüfen den Typ und die Form eines Wertes, nicht seinen exakten Inhalt, und trennen damit strukturelle Erwartungen von konkreten Testdaten.
Neben like() für beliebige Werte desselben Typs bietet Pact.js integer(), string(), eachLike() für Arrays mit einer wiederkehrenden Struktur, und regex() für Werte, die einem bestimmten Muster folgen müssen, etwa ein UUID-Format oder ein ISO-Datum. Die korrekte Wahl der Matcher ist entscheidend für die Stabilität von Contract Testing: Zu strenge Matcher erzeugen unnötige Fehlschläge bei irrelevanten Datenänderungen, zu lockere Matcher übersehen echte strukturelle Brüche.
import { MatchersV3 } from '@pact-foundation/pact';
const { like, eachLike, regex, integer, iso8601DateTime } = MatchersV3;
// Contract expecting a list of orders with a repeating structure
const orderListExpectation = {
status: 200,
body: eachLike({
orderId: regex('^ORD-[0-9]{6}$', 'ORD-000123'),
total: like(129.5),
createdAt: iso8601DateTime('2026-07-30T10:00:00Z'),
itemCount: integer(3),
}),
};
// eachLike() tells the provider verification: "expect an array where
// every element matches this shape" — not a fixed number of items
5. Provider-Verifikation auf Backend-Seite
Auf Backend-Seite liest die Provider-Verifikation die vom Frontend erzeugte Pact-Datei ein und spielt jeden darin beschriebenen Request gegen die echte, laufende Backend-Anwendung ab. Für jeden erwarteten Zustand, etwa "ein Produkt mit ID 42 existiert", muss das Backend über sogenannte Provider States Testdaten bereitstellen, die diesen Zustand tatsächlich abbilden. Ohne korrekt eingerichtete Provider States würde die Verifikation scheitern, nicht weil der Vertrag gebrochen wäre, sondern weil die Testumgebung des Backends den vorausgesetzten Zustand nicht herstellt.
Der entscheidende Vorteil von Contract Testing zeigt sich hier: Das Backend-Team muss das Frontend-Repository nicht kennen oder einsehen, um zu wissen, was getestet werden muss. Die Pact-Datei ist self-contained und beschreibt präzise, welche Requests mit welchen Responses erwartet werden. Diese Entkopplung erlaubt es Frontend- und Backend-Teams, unabhängig voneinander zu arbeiten, während die Pact-Datei als einziger, verbindlicher Kommunikationskanal über die tatsächlich genutzte Schnittstelle fungiert.
// tests/contracts/provider-verification.test.js — runs on the backend repository
import { Verifier } from '@pact-foundation/pact';
import { startTestServer, seedProduct } from '../helpers/server.js';
describe('Pact Provider Verification', () => {
it('validates the catalog-backend against all consumer contracts', async () => {
const server = await startTestServer();
const opts = {
provider: 'catalog-backend',
providerBaseUrl: `http://localhost:${server.port}`,
pactUrls: ['./pacts/storefront-frontend-catalog-backend.json'],
stateHandlers: {
'a product with id 42 exists': async () => {
await seedProduct({ id: 42, name: 'Wireless Keyboard', price: 49.99 });
},
},
};
await new Verifier(opts).verifyProvider();
await server.close();
});
});
6. Pact Broker: Verträge zentral austauschen
In größeren Organisationen mit vielen Consumer- und Provider-Teams wird das manuelle Verteilen von Pact-Dateien schnell unpraktikabel. Der Pact Broker ist ein zentraler Dienst, bei dem Consumer ihre generierten Pact-Dateien nach jedem erfolgreichen Testlauf automatisch hochladen und Provider ihre Verifikationsergebnisse zurückmelden. Für Contract Testing zwischen Frontend und Backend in größeren Setups ist der Broker praktisch unverzichtbar, weil er eine durchsuchbare Matrix bietet, welcher Consumer mit welcher Provider-Version kompatibel ist.
Der Broker visualisiert zudem sogenannte Contract-Netzwerke: Wer konsumiert welche API, welche Version wurde zuletzt erfolgreich verifiziert, und welche Kombinationen sind aktuell inkompatibel. Diese Transparenz ist besonders wertvoll in Microservice-Architekturen mit vielen unabhängig deploybaren Frontends und Backends, wo eine zentrale Übersicht über Kompatibilität sonst nur schwer herzustellen wäre.
7. can-i-deploy: Deployments sicher absichern
Ein besonders praxisrelevantes Feature von Contract Testing mit Pact ist der can-i-deploy Befehl. Bevor ein Frontend oder Backend in eine bestimmte Umgebung deployt wird, prüft dieser Befehl gegen den Pact Broker, ob die aktuelle Version mit allen bereits deployten Gegenstücken kompatibel ist. Ist die Antwort nein, verhindert die CI-Pipeline das Deployment automatisch, bevor eine inkompatible Kombination aus Frontend und Backend live geht.
# .gitlab-ci.yml — deployment gate using can-i-deploy
deploy-check:
stage: pre-deploy
script:
- npx pact-broker can-i-deploy
--pacticipant storefront-frontend
--version $CI_COMMIT_SHA
--to-environment production
--broker-base-url $PACT_BROKER_URL
--broker-token $PACT_BROKER_TOKEN
rules:
- if: '$CI_COMMIT_BRANCH == "main"'
deploy-production:
stage: deploy
needs: ['deploy-check']
script:
- ./scripts/deploy.sh production
Diese Absicherung ist der praktische Kern von Contract Testing zwischen Frontend und Backend: Statt sich auf manuelle Koordination oder eine gemeinsame Release-Ankündigung zu verlassen, verhindert die Pipeline automatisiert inkompatible Deployments. Teams können unabhängig und häufig deployen, ohne die Kompatibilität manuell zwischen mehreren Repositories abstimmen zu müssen.
8. Grenzen: Wann Contract Testing nicht ausreicht
Contract Testing prüft strukturelle und semantische Kompatibilität einer Schnittstelle, nicht die fachliche Korrektheit der zugrunde liegenden Geschäftslogik. Ein Backend kann einen Vertrag perfekt erfüllen und trotzdem einen falschen Preis berechnen. Für fachliche Korrektheit bleiben klassische Unit- und Integrationstests auf Backend-Seite notwendig, Contract Testing ersetzt sie nicht.
Ein zweiter Grenzfall ist der initiale Aufwand. Für kleine Teams mit einem einzigen Frontend und einem einzigen Backend im selben Repository ist der Mehrwert von Contract Testing gering, weil Frontend und Backend ohnehin gemeinsam deployt werden und Inkompatibilitäten sofort auffallen. Der Nutzen steigt deutlich mit der Anzahl unabhängiger Teams, Repositories und Deployment-Zyklen, weil genau dort implizite Annahmen am ehesten unbemerkt auseinanderdriften.
9. Contract Testing im Vergleich zu Alternativen
Die folgende Tabelle stellt Contract Testing anderen Ansätzen zur Absicherung von Schnittstellen gegenüber.
| Ansatz | Prüft tatsächliche Nutzung | Erkennt Breaking Changes früh | Geeignet für |
|---|---|---|---|
| Manuelle Absprache | Nein, fehleranfällig | Nein, nur reaktiv | Sehr kleine, eng abgestimmte Teams |
| OpenAPI-Spezifikation | Teilweise, provider-zentriert | Bedingt, ohne Consumer-Bezug | Dokumentation, Codegenerierung |
| E2E-Tests gegen Staging | Ja, aber langsam | Erst nach gemeinsamem Deployment | Vollständige Systemvalidierung |
| Contract Testing (Pact.js) | Ja, consumer-getrieben | Ja, vor dem Deployment | Unabhängige Teams, häufige Deploys |
Contract Testing ersetzt weder OpenAPI-Dokumentation noch E2E-Tests vollständig, ergänzt sie aber um eine schnelle, gezielte Absicherung genau der Schnittstellenpunkte, die tatsächlich von einem Consumer genutzt werden, ohne auf einen langsamen, gemeinsamen Staging-Test warten zu müssen.
Mironsoft
API-Vertragssicherheit für Magento- und Headless-Frontends
Breaking Changes zwischen Frontend und Backend verhindern?
Wir richten Contract Testing mit Pact.js zwischen euren Frontend- und Backend-Teams ein, verbinden es mit einem Pact Broker und integrieren can-i-deploy als Deployment-Gate in eure CI-Pipeline.
Pact.js Einführung
Consumer-Tests und Provider-Verifikation für kritische Schnittstellen aufsetzen
Broker & CI-Gate
Pact Broker aufsetzen und can-i-deploy als Deployment-Absicherung integrieren
Team-Workflow
Prozesse für unabhängiges Deployen zwischen Frontend- und Backend-Teams etablieren
10. Zusammenfassung
Contract Testing zwischen Frontend und Backend macht implizite Annahmen über eine gemeinsam genutzte API explizit und maschinell prüfbar. Der Consumer, meist das Frontend, definiert seine Erwartungen als ausführbaren Test, der automatisch eine Pact-Datei erzeugt. Der Provider, meist das Backend, verifiziert automatisiert, ob seine echte Implementierung diesem Vertrag entspricht, ohne das Frontend-Repository kennen zu müssen. Matcher wie like(), eachLike() und regex() trennen strukturelle Erwartungen von konkreten Testdaten und machen Verträge robust gegen irrelevante Wertänderungen.
Der Pact Broker zentralisiert den Austausch von Verträgen und Verifikationsergebnissen zwischen vielen Teams, während can-i-deploy inkompatible Deployments direkt in der CI-Pipeline verhindert. Contract Testing ersetzt weder fachliche Backend-Tests noch vollständige E2E-Tests, ergänzt sie aber um eine schnelle, gezielte Absicherung genau der Schnittstellenpunkte, die tatsächlich genutzt werden, und ist besonders wertvoll für Teams mit unabhängigen Deployment-Zyklen.
Contract Testing zwischen Frontend und Backend — Das Wichtigste auf einen Blick
Consumer Driven Contract
Das Frontend definiert seine Erwartungen, das Backend verifiziert automatisch dagegen.
Matcher
like(), eachLike() und regex() trennen Struktur von konkreten Testwerten für robuste Verträge.
Pact Broker
Zentraler Austausch von Verträgen und Verifikationsergebnissen zwischen mehreren Teams.
can-i-deploy
Verhindert inkompatible Deployments automatisch direkt in der CI-Pipeline.