Contract Testing zwischen Frontend und Backend mit Pact.js
AI generated
JS
() =>
JavaScript · Testing Advanced · Pact.js · API-Verträge
Contract Testing zwischen Frontend und Backend
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.

18 Min. Lesezeit Pact.js · Consumer Driven Contracts · CI Node.js 20+ · beliebiges Backend

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.

11. FAQ: Contract Testing zwischen Frontend und Backend

1Was ist Contract Testing?
Prüft, ob ein Backend eine Schnittstelle so implementiert, wie ein Frontend sie tatsächlich erwartet.
2Unterschied zu MSW?
MSW prüft Frontend-Reaktion, Contract Testing prüft zusätzlich, ob das echte Backend die Antwort liefert.
3Was ist ein Consumer Driven Contract?
Ein vom Consumer definierter Vertrag, der genau dessen tatsächliche Anforderungen an die API beschreibt.
4Warum Matcher statt exakter Werte?
Matcher prüfen Typ und Form, nicht exakten Inhalt, und verhindern Fehlschläge bei irrelevanten Datenunterschieden.
5Muss Backend Frontend-Code kennen?
Nein, die Pact-Datei ist self-contained und beschreibt alle erwarteten Requests und Responses.
6Wozu der Pact Broker?
Zentralisiert Verträge und Verifikationsergebnisse zwischen vielen Teams mit durchsuchbarer Kompatibilitätsmatrix.
7Was macht can-i-deploy?
Prüft Kompatibilität vor dem Deployment und verhindert inkompatible Kombinationen automatisch.
8Ersetzt es E2E-Tests?
Nein, es prüft strukturelle Kompatibilität, nicht vollständige fachliche Korrektheit im Systemzusammenhang.
9Lohnt es sich für kleine Teams?
Weniger, wenn gemeinsam deployt wird. Hoher Nutzen bei unabhängigen Teams mit getrennten Deployment-Zyklen.
10Funktioniert es sprachübergreifend?
Ja, das Pact-Dateiformat ist sprachagnostisch und funktioniert mit jedem Backend, für das ein Pact-Verifier existiert.