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.
FAIL e2e/checkout.spec.ts
✕ Bestellung abschließen (30014 ms)
TimeoutError: waiting for selector "#order-confirmation" failed:
timeout 30000ms exceeded
Tests: 1 failed, 213 passed Dauer: 46m 08s
Dieser Fehlschlag sagt nichts darüber aus, ob der Code kaputt ist. Er sagt, dass irgendwo im hochgefahrenen Gesamtsystem etwas dreißig Sekunden gebraucht hat statt drei – ein Container, der träge startet, eine Animation, ein Netzwerk-Hop. Beim nächsten Lauf ist derselbe Test grün, ohne dass eine Zeile geändert wurde. Genau so verliert eine nächtliche End-to-End-Suite ihre Autorität: Sie beantwortet die Frage, ob heute deployt werden darf, mit einem Wahrscheinlichkeitswurf, und irgendwann klickt das Team die roten Läufe einfach weg.
Der E2E-Marathon – die Suite, die nachts das komplette System hochfährt, sich durch echte UIs klickt und morgens rot oder grün dasteht – ist deshalb kein Disziplinproblem. Er scheitert an seiner Struktur. Und die Frage, um die es eigentlich geht, ist viel kleiner als das, was er prüft: Passen diese beiden Services noch zusammen? Die Mechanik der Antwort ist schnell erklärt; der Gewinn liegt woanders.
Der wesentliche Unterschied liegt nicht in der Anzahl der Tests, sondern im Ort, an dem Kompatibilität geprüft wird.
Gemeinsame E2E-Umgebung
- Aufbau · alle beteiligten Systeme gleichzeitig starten
- Signal · ein kompletter Pfad ist gerade erfolgreich durchgelaufen
- Schwäche · Fehlerursache, Timing und verantwortliche Grenze bleiben oft unklar
- Kopplung · Releases warten auf denselben integrierten Zustand
Consumer-Driven Contract
- Aufbau · Erwartungen an einer Servicegrenze aufzeichnen
- Signal · Consumer und Provider erfüllen denselben Vertrag unabhängig
- Stärke · schneller, deterministischer und einer Grenze zuordenbar
- Grenze · ersetzt keine kleine Auswahl echter End-to-End-Prüfungen
Der Vergleich streicht E2E-Tests nicht. Er verschiebt die häufige Kompatibilitätsprüfung an die Stelle, an der der Vertrag tatsächlich entsteht.
Warum der E2E-Marathon strukturell scheitert
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.
In der klassischen Testpyramide wird die Rolle sofort sichtbar: 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
Konkret sieht das so aus. 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 Effekt ist das eigentliche Argument. 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
Consumer-Driven Contracts ersetzen den E2E-Marathon – nicht jeden E2E-Test. Wer sie als Wundermittel verkauft, verspricht zu viel.
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 bleibt Stückwerk. Verträge, die nur lokal laufen und nicht versioniert zwischen den Pipelines ausgetauscht werden, geben keine verlässliche Deploy-Freigabe. Vor allem aber sind Consumer-Driven Contracts genauso sehr eine Frage der Zusammenarbeit wie der Technik: Beide Teams müssen sich auf den Vertrag als verbindliche Absprache einlassen. 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 taucht am Ende in keiner Test-Metrik auf, er zeigt sich in der Auslieferung: 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.
Weiterführende Quellen
- Consumer-Driven Contracts: A Service Evolution Pattern (Ian Robinson, martinfowler.com)
- The Practical Test Pyramid (Ham Vocke, martinfowler.com)
- TestPyramid (Martin Fowler, Bliki)
- DORA: Loosely coupled architecture
- DORA: Continuous delivery
- pact-jvm (pact-foundation, GitHub)
- Forsgren, Humble, Kim: Accelerate (2018)
Wie fandest du diesen Beitrag?
Kommentare