post

Contract Testing statt E2E-Marathon

Consumer-Driven Contracts prüfen Schnittstellen an der Grenze zwischen Services statt in einer teuren, flaky End-to-End-Suite. Das Ergebnis: ein schnelleres, deterministisches Deploy-Gate und Teams, die unabhängig releasen.

≈ 8 Min. Lesezeit

Es gibt ein Muster, das ich in fast jedem Beratungsmandat wiederfinde, sobald aus einem Monolithen mehrere Services geworden sind: die nächtliche End-to-End-Suite. Sie fährt das komplette System hoch, klickt sich durch echte UIs, ruft echte Backends auf, und am Morgen steht in Rot oder Grün, ob heute deployt werden darf. In der Theorie ist das die ultimative Absicherung. In der Praxis ist es ein Marathon, der den ganzen Delivery-Fluss ausbremst und dem am Ende niemand mehr vertraut.

Ich will in diesem Artikel zeigen, warum diese E2E-Marathons ein strukturelles Problem sind und nicht nur eine Frage der Testdisziplin, und warum Consumer-Driven Contracts die deutlich elegantere Antwort auf die eigentliche Frage geben: "Passen diese beiden Services noch zusammen?" Es geht mir dabei weniger um ein Tool als um eine Strategie. Die Mechanik ist schnell erklärt; der Gewinn liegt woanders.

Warum der E2E-Marathon strukturell scheitert

Fangen wir beim Outcome an. Was ein Team wirklich braucht, ist eine schnelle, verlässliche Antwort auf die Frage, ob ein neuer Release die Zusammenarbeit zwischen Services bricht. Genau diese Antwort liefert eine große E2E-Suite am schlechtesten.

Martin Fowler und Ham Vocke haben das in "The Practical Test Pyramid" präzise auf den Punkt gebracht: UI- und End-to-End-Tests sind "brittle, expensive to write, and time consuming to run". Sie sind anfällig für Nicht-Determinismus – Timing, Animationen, Browser-Quirks, ein Netzwerk-Hop, der mal 50 und mal 800 Millisekunden braucht. Und genau dieser Nicht-Determinismus ist das Gift: Sobald eine Suite in einem von zwanzig Läufen aus Gründen rot wird, die nichts mit dem Code zu tun haben, fängt das Team an, Fehlschläge wegzuklicken. Der Test, der Vertrauen schaffen sollte, produziert das Gegenteil.

Dazu kommt der Koppelungseffekt. Eine E2E-Suite braucht alle beteiligten Services gleichzeitig, in kompatiblen Versionen, hochgefahren im selben Environment. Damit erzwingt sie einen Lockstep: Team A kann erst deployen, wenn die gemeinsame Suite mit Team Bs aktuellem Stand grün ist. Man hat die Services technisch entkoppelt und sie sich durch die Testinfrastruktur wieder aneinandergekettet.

Die DORA-Forschung im Buch "Accelerate" von Forsgren, Humble und Kim liefert dazu die empirische Gegenseite. Zwei der stärksten Prädiktoren für erfolgreiche Continuous Delivery sind eine "loosely coupled architecture" und "continuous testing". Elite-Performer können ihre Services unabhängig voneinander ändern und deployen, ohne mit anderen Teams synchronisieren zu müssen. Der E2E-Marathon arbeitet gegen genau dieses Ziel. Er ist der synchronisierte Lockstep in Testform.

Die Grenze prüfen, nicht das ganze System

Die entscheidende Einsicht ist eine der Testabstraktion. Wenn zwei Services zusammenarbeiten, muss ich nicht das gesamte integrierte System hochfahren, um zu prüfen, ob sie sich verstehen. Ich muss nur prüfen, ob die Grenze zwischen ihnen stimmt – ob der Consumer das schickt, was der Provider erwartet, und der Provider das zurückgibt, worauf der Consumer sich verlässt.

Fowlers Definition ist hier zitierfähig präzise: "An integration contract test is a test at the boundary of an external service verifying that it meets the contract expected by a consuming service." Der Consumer testet gegen einen Mock, der Provider verifiziert die aufgezeichneten Interaktionen – und beide Seiten laufen vollständig isoliert. Kein gemeinsames Environment, kein Lockstep.

Das Muster dahinter ist alles andere als neu. Ian Robinson hat "Consumer-Driven Contracts: A Service Evolution Pattern" bereits 2006 auf martinfowler.com beschrieben. Der Consumer definiert seine Erwartungen; der Provider verifiziert gegen die Vereinigung – die Union – aller Consumer-Erwartungen. Das ist kein Hype, das ist ein rund siebzehn Jahre reifes Muster. Was sich geändert hat, ist die Werkzeuglandschaft, die es heute pragmatisch in eine CI-Pipeline gießt.

Ich ordne das gern in die klassische Testpyramide ein, weil das die Rolle sofort klarmacht: Contract Tests sitzen in der Mitte, als schneller Isolationstest zwischen den vielen Unit-Tests der Basis und der schmalen, teuren E2E-Spitze.

flowchart TB
    subgraph Pyramid["Test Pyramid"]
        direction TB
        E2E["E2E<br/>slow / flaky<br/>few"]
        CT["Integration / Contract<br/>fast / isolated<br/>focus"]
        UNIT["Unit<br/>fast / deterministic<br/>many"]
        UNIT --> CT --> E2E
    end

Die Botschaft der Pyramide ist nicht "keine E2E-Tests". Sie ist: Prüfe die Kompatibilität da, wo sie entsteht – an der Grenze – und nicht durch ein hochgefahrenes Gesamtsystem an der Spitze.

Wie ein Contract entsteht

Schauen wir uns an, wie das konkret aussieht. Der Consumer – sagen wir eine WebApp – beschreibt in einem Test, was er vom Provider, hier einem OrderService, erwartet. Dieser Test läuft gegen einen lokalen Mock und generiert dabei als Nebenprodukt eine Vertragsdatei, den Pact.

const provider = new Pact({ consumer: 'WebApp', provider: 'OrderService' });

provider
  .given('an order with id 42 exists')          // provider state
  .uponReceiving('a request for order 42')
  .withRequest({ method: 'GET', path: '/orders/42' })
  .willRespondWith({
    status: 200,
    body: {
      id: like(42),                              // type matcher
      status: term({ matcher: 'PAID|OPEN', generate: 'PAID' }),
      total: like(19.99),
    },
  });
// the consumer runs against the Pact mock, then publishes the pact file to the broker

Zwei Details sind hier wichtig, weil sie den Unterschied zwischen "brauchbar" und "brüchig" ausmachen. Erstens die Matcher. like(42) prüft nicht auf den exakten Wert 42, sondern auf den Typ – eine Zahl. term(...) prüft gegen ein Muster. Damit sagt der Consumer aus, was er strukturell braucht, statt sich an konkreten Beispielwerten festzuklammern. Zu lax gesetzte Matcher geben falsche Sicherheit, zu strikte machen den Test brüchig; die richtige Körnung ist Handwerk.

Zweitens der Provider State, das given('an order with id 42 exists'). Der Consumer beschreibt damit die Vorbedingung, unter der seine Erwartung gilt. Und genau hier liegt, ehrlich gesagt, der Hauptaufwand des gesamten Ansatzes – dazu gleich mehr.

Auf der Providerseite wird der Vertrag dann nicht neu geschrieben, sondern verifiziert. Der Provider stellt jeden geforderten State real her und spielt die aufgezeichneten Interaktionen gegen seine echte Implementierung.

@Provider("OrderService")
@PactBroker
class OrderServiceContractTest {

    @State("an order with id 42 exists")
    void orderExists() {
        // set up the provider state so the recorded interaction is reproducible
        orderRepository.save(new Order(42, "PAID", 19.99));
    }
}

Beide Seiten laufen isoliert in ihrer eigenen Pipeline. Der Consumer weiß nichts von der Providerimplementierung, der Provider fährt keine WebApp hoch. Und trotzdem entsteht eine belastbare Aussage über ihre Kompatibilität.

Das Deploy-Gate: eine deterministische Ja/Nein-Frage

Jetzt kommt der Teil, der den Marathon tatsächlich ersetzt. Die Verträge und die Verifikationsergebnisse landen in einem zentralen Pact Broker. Damit lässt sich vor jedem Release eine simple, deterministische Frage stellen: Sind diese konkreten Versionen von Consumer und Provider miteinander kompatibel?

# provider side: verify the recorded interactions and publish results to the broker
pact-provider-verifier --provider OrderService --publish-verification-results

# both sides gate their release on a deterministic check
pact-broker can-i-deploy \
  --pacticipant WebApp --version $GIT_SHA \
  --to-environment production

can-i-deploy ist der Kern. Statt "wir warten, bis die nächtliche E2E-Suite grün ist" bekommt die Pipeline eine sofortige, reproduzierbare Antwort aus dem Broker: Ja, diese Version der WebApp ist gegen die aktuell in Produktion laufende Version des OrderService verifiziert – deploy. Oder: Nein, es fehlt eine Verifikation – halt an.

Der ganze Ablauf ist eine Prüfung an der Grenze, kein hochgefahrenes Gesamtsystem:

sequenceDiagram
    participant C as Consumer (WebApp)
    participant B as Pact Broker
    participant P as Provider (OrderService)
    C->>B: publish pact (from mock test)
    P->>B: verify pact, publish results
    C->>B: can-i-deploy? (WebApp @ sha)
    B-->>C: yes -> deploy
    P->>B: can-i-deploy? (OrderService @ sha)
    B-->>P: yes -> deploy

Der Outcome ist es, der zählt. Feedback in Minuten statt über Nacht. Ein deterministisches Gate statt eines flaky Wahrscheinlichkeitswurfs. Und – das ist der eigentliche Hebel – jedes Team kann unabhängig deployen, sobald sein Teil des Vertrags erfüllt ist. Kein Lockstep, kein "wir müssen erst mit Team B synchronisieren". Genau die lose Koppelung, die "Accelerate" als Prädiktor für Delivery-Performance identifiziert, wird hier testbar.

Wo Contract Testing aufhört – die ehrliche Abgrenzung

Ich würde meinem eigenen Anspruch nicht gerecht, wenn ich das als Wundermittel verkaufte. Consumer-Driven Contracts ersetzen den E2E-Marathon – nicht jeden E2E-Test.

Der wichtigste Vorbehalt zuerst: Contract Tests prüfen Schnittstellen-Kompatibilität. Struktur, Felder, Status-Codes, das Format an der Grenze. Sie prüfen nicht echte End-to-End-Business-Flows über mehrere Hops, nicht das UI-Verhalten, nicht Netzwerk- und Infrastruktureffekte im Zusammenspiel. Ein schmales E2E-Smoke-Set, das einmal den kritischen Pfad durch das laufende Gesamtsystem zieht, bleibt sinnvoll und richtig. Die These lautet "statt E2E-Marathon", nicht "null E2E". Man tauscht eine breite, langsame, unzuverlässige Suite gegen viele schnelle, isolierte Verträge plus eine Handvoll gezielter Smoke-Tests.

Der zweite Punkt ist der, an dem ich in der Praxis die meiste Zeit verbringe: die Provider States. Jedes given('...') muss providerseitig real herstellbar sein. Driften die Annahmen des Consumers und das tatsächliche Setup des Providers auseinander, wird der vermeintlich leichtgewichtige Test doch aufwändig. Hier liegt die Wartungsarbeit, und wer sie unterschätzt, wird enttäuscht. Es lohnt sich, die States bewusst schlank und wiederverwendbar zu halten.

Und der dritte Punkt ist gar kein technischer. Contract Testing ohne Broker und ohne can-i-deploy im CI ist halbe Miete. Verträge, die nur lokal laufen und nicht versioniert zwischen den Pipelines ausgetauscht werden, geben keine verlässliche Deploy-Freigabe. Vor allem aber ist Consumer-Driven Contracts genauso viel Sozio- wie Technik: Beide Teams müssen sich auf den Vertrag als verbindliche Absprache committen. Der Consumer sagt, was er braucht; der Provider verpflichtet sich, es zu liefern und nicht kaputtzumachen. Das ist eine organisatorische Vereinbarung, keine Bibliothek. Wenn die Kultur nicht mitspielt, hilft das beste Tool nicht.

Ein Wort zur Werkzeugwahl, ohne in Produktwerbung zu verfallen: Im JS-Umfeld generiert Pact die Verträge; als Consumer-Mocks laufen dort WireMock, Nock oder MSW. Auf der JVM ist Spring Cloud Contract eine etablierte Alternative zu pact-jvm. Der Pact Broker ist Open Source und trägt die CI-Mechanik. Es gibt kommerzielle Erweiterungen im Umfeld – für den hier beschriebenen Kern braucht man sie nicht. Die Strategie steht und fällt nicht mit dem Vendor, sondern mit der Disziplin, an der Grenze zu prüfen und sich als Teams auf Verträge zu einigen.

Fazit

Der E2E-Marathon ist kein Testproblem, sondern ein Architekturproblem in Testform: Er koppelt Services über die Infrastruktur wieder aneinander, die man mit gutem Grund entkoppelt hat, und tauscht dafür Determinismus und Geschwindigkeit gegen ein trügerisches Gefühl von Vollständigkeit ein. Consumer-Driven Contracts drehen das um. Sie prüfen Kompatibilität da, wo sie entsteht – an der Grenze zwischen zwei Services –, isoliert, schnell und reproduzierbar. Das Deploy-Gate wird zu einer Ja/Nein-Frage, die in Minuten beantwortet ist, und jedes Team gewinnt die Freiheit zurück, unabhängig zu releasen.

Der Gewinn ist am Ende keine Test-Metrik, sondern ein Delivery-Outcome: kürzere Zyklen, weniger Reibung zwischen Teams, höhere Verlässlichkeit. Ein schmales E2E-Smoke-Set bleibt daneben bestehen – aber der Marathon darf gehen. Man tauscht ein großes, langsames, unzuverlässiges Sicherheitsnetz gegen viele kleine, präzise gespannte. Einfach und elegant statt Prozess-Overhead, und genau das ist der Punkt.

Weiterführende Quellen

Kommentare