Contract Testing für Symfony APIs: Breaking Changes vor dem Deployment erkennen
AI generated
SF
{ }
Symfony · API · Contract Testing · PACT
Contract Testing für Symfony APIs
Breaking Changes vor dem Deployment erkennen

Ein grüner End-to-End-Test beweist nicht, dass alle Consumer einer Symfony API weiterhin funktionieren, wenn sich ein Response-Feld ändert. Contract Testing macht die Erwartungen jedes Consumers explizit und prüft sie automatisch gegen den echten Provider, lange bevor ein Breaking Change in Produktion Schaden anrichtet.

19 Min. Lesezeit PACT · OpenAPI-Validierung · Consumer Driven Contracts Symfony 7 · API Platform

1. Warum End-to-End-Tests Breaking Changes nicht zuverlässig fangen

Contract Testing löst ein Problem, das in Microservice-Landschaften und bei mehreren API-Consumern regelmäßig auftritt: Ein Team ändert eine Symfony API, alle eigenen Tests bleiben grün, aber ein externer Consumer, etwa eine mobile App oder ein Partner-System, bricht in Produktion, weil er sich auf ein Detail verlassen hat, das nirgends explizit dokumentiert war. End-to-End-Tests gegen die eigene Codebasis prüfen naturgemäß nicht das Verhalten fremder Consumer, die man oft nicht einmal kennt.

Der Kern von Contract Testing ist, diese impliziten Erwartungen explizit zu machen: Jeder Consumer definiert einen Contract, der genau beschreibt, welche Felder er aus welcher Response erwartet und mit welchen Anfragen er die API aufruft. Dieser Contract wird anschließend automatisch gegen den echten Provider verifiziert, unabhängig davon, ob Consumer und Provider im selben Repository, demselben Team oder derselben Organisation liegen.

Dieser Artikel zeigt, wie man mit PACT einen Consumer Driven Contract für eine Symfony API erzeugt, wie die Provider-Verifikation funktioniert, wie OpenAPI-Schema-Validierung als leichtgewichtige Alternative dient und wie Contract Tests in die CI-Pipeline integriert werden, ohne Deployments zu verlangsamen.

2. Consumer Driven Contracts: das Grundprinzip

Bei einem Consumer Driven Contract schreibt nicht das Provider-Team, was die API zurückgibt, sondern jedes Consumer-Team beschreibt, was es tatsächlich braucht. Diese Umkehrung ist entscheidend: Ein Provider kann Felder hinzufügen, umbenennen oder entfernen, die kein Consumer nutzt, ohne dass irgendein Contract-Test fehlschlägt. Nur Änderungen, die tatsächlich verwendete Felder betreffen, lösen einen roten Test aus, was False Positives drastisch reduziert im Vergleich zu einer vollständigen Schema-Validierung, die jede Abweichung als Fehler wertet.

In der Praxis bedeutet das für eine Symfony API mit mehreren Consumern: Eine mobile App definiert ihren eigenen Contract mit den Feldern, die ihre UI tatsächlich rendert. Ein Partner-System definiert einen separaten Contract mit den Feldern, die dessen Integration benötigt. Beide Contracts werden unabhängig voneinander gegen denselben Provider-Endpunkt verifiziert, wodurch das Provider-Team genau sieht, welche Änderung welchen Consumer betreffen würde, bevor überhaupt deployt wird.

3. PACT-Contract aus Consumer-Sicht erzeugen

PACT ist das verbreitetste Framework für Consumer Driven Contract Testing und bringt eine PHP-Implementierung mit, die sich in Symfony Consumer-Tests einbinden lässt. Der Consumer-Test simuliert die Interaktion mit der API gegen einen von PACT gestarteten Mock-Server, definiert die erwartete Anfrage und Response, und PACT erzeugt daraus eine Contract-Datei im JSON-Format, die exakt dokumentiert, was der Consumer erwartet.

Wichtig ist, dass der Consumer-Test gegen den PACT-Mock-Server läuft, nicht gegen die echte Symfony API. Das hält Consumer-Tests schnell und unabhängig vom Provider-Deployment. Die erzeugte Contract-Datei wird anschließend, meist über einen Pact Broker, an das Provider-Team übergeben, das die Contracts aller Consumer gegen seine tatsächliche Implementierung verifiziert.


<?php

declare(strict_types=1);

namespace App\Tests\Contract\Consumer;

use PhpPact\Consumer\Model\ConsumerRequest;
use PhpPact\Consumer\Model\ProviderResponse;
use PhpPact\Standalone\MockService\MockServerEnvConfig;
use PhpPact\Standalone\MockService\MockServerHttpClientConfig;
use PHPUnit\Framework\TestCase;

final class ProductApiConsumerContractTest extends TestCase
{
    public function testMobileAppExpectsProductNameAndPrice(): void
    {
        $request = new ConsumerRequest();
        $request->setMethod('GET')
            ->setPath('/api/products/42')
            ->addHeader('Accept', 'application/json');

        $response = new ProviderResponse();
        $response->setStatus(200)
            ->addHeader('Content-Type', 'application/json')
            ->setBody(['id' => 42, 'name' => 'Test Product', 'price' => 19.99]);

        $config = new MockServerEnvConfig();
        $builder = new \PhpPact\Consumer\InteractionBuilder($config);
        $builder->uponReceiving('a request for a product used in the mobile app')
            ->with($request)
            ->willRespondWith($response);

        $builder->run(function () use ($config) {
            $client = new \GuzzleHttp\Client(['base_uri' => $config->getBaseUri()]);
            $response = $client->get('/api/products/42', ['headers' => ['Accept' => 'application/json']]);
            self::assertSame(200, $response->getStatusCode());
        });
    }
}

4. Provider-Verifikation gegen den Contract in Symfony

Die Provider-Verifikation ist der zweite, gleichermaßen wichtige Teil des Contract Testings. Das Symfony Provider-Team startet die echte Anwendung, meist mit einer Testdatenbank und Fixtures, die die im Contract erwarteten Daten bereitstellen, und lässt PACT jede Interaktion aus jedem Consumer-Contract gegen diese laufende Anwendung abspielen. Weicht die tatsächliche Response von der im Contract definierten Erwartung ab, schlägt die Verifikation fehl, noch bevor der Code gemergt wird.

Ein entscheidender Vorteil dieser Richtung: Das Provider-Team sieht in einem einzigen CI-Lauf, welche der registrierten Consumer-Contracts durch eine geplante Änderung betroffen wären. Das ersetzt das mühsame manuelle Nachfragen bei jedem Consumer-Team, ob eine geplante API-Änderung Probleme verursachen könnte, durch eine automatisierte, deterministische Prüfung.


<?php

declare(strict_types=1);

namespace App\Tests\Contract\Provider;

use PhpPact\Standalone\ProviderVerifier\Model\VerifierConfig;
use PhpPact\Standalone\ProviderVerifier\Verifier;
use PHPUnit\Framework\TestCase;

final class ProductApiProviderVerificationTest extends TestCase
{
    public function testProductApiSatisfiesAllRegisteredContracts(): void
    {
        $config = (new VerifierConfig())
            ->setProviderName('product-api')
            ->setProviderBaseUrl('http://127.0.0.1:8000')
            ->setPactBrokerBaseUri('https://pact-broker.mironsoft.internal')
            ->setPublishResults(true)
            ->setProviderVersion(getenv('CI_COMMIT_SHA') ?: 'local');

        $verifier = new Verifier($config);
        $verifier->verify();

        self::assertTrue(true); // verify() throws on any contract mismatch
    }
}

5. OpenAPI-Schema-Validierung als leichtgewichtige Alternative

Nicht jedes Symfony Projekt braucht die volle PACT-Infrastruktur mit Broker und separaten Consumer-Repositories. Wenn API Platform oder NelmioApiDocBundle bereits eine OpenAPI-Spezifikation generieren, lässt sich diese Spezifikation direkt gegen die tatsächlichen API-Responses validieren, mit Bibliotheken wie league/openapi-psr7-validator. Das ist kein vollständiges Consumer Driven Contract Testing, aber ein pragmatischer Zwischenschritt, der immerhin sicherstellt, dass die API ihre eigene, dokumentierte Spezifikation einhält.

Der Unterschied zur PACT-Strategie: OpenAPI-Validierung prüft gegen ein zentrales Schema, das vom Provider definiert wird, nicht gegen die tatsächlichen Erwartungen einzelner Consumer. Ein Feld, das im Schema als optional markiert ist, aber von einem bestimmten Consumer zwingend benötigt wird, fällt bei reiner Schema-Validierung nicht auf. Für Projekte mit wenigen, gut bekannten Consumern reicht diese leichtere Strategie oft aus, für viele externe Consumer ist echtes Contract Testing die robustere Wahl.


<?php

declare(strict_types=1);

namespace App\Tests\Functional\OpenApi;

use League\OpenAPIValidation\PSR7\ValidatorBuilder;
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
use Symfony\Bridge\PsrHttpMessage\Factory\PsrHttpFactory;

final class ProductEndpointOpenApiComplianceTest extends WebTestCase
{
    public function testProductResponseMatchesOpenApiSchema(): void
    {
        $client = static::createClient();
        $client->request('GET', '/api/products/42');

        $validator = (new ValidatorBuilder())
            ->fromYamlFile(__DIR__ . '/../../../public/openapi.yaml')
            ->getResponseValidator();

        $psrResponse = $this->toPsrResponse($client->getResponse());

        // Throws on schema mismatch — turns undocumented drift into a failing test.
        $validator->validate(
            new \League\OpenAPIValidation\PSR7\OperationAddress('/api/products/{id}', 'get'),
            $psrResponse
        );

        self::assertTrue(true);
    }
}

6. Contract-Versionierung mit dem Pact Broker

Der Pact Broker ist der zentrale Dreh- und Angelpunkt, an dem Consumer-Contracts veröffentlicht und Provider-Verifikationsergebnisse gespeichert werden. Ohne einen Broker müssten Contract-Dateien manuell zwischen Repositories kopiert werden, was in der Praxis schnell veraltet und inkonsistent wird. Mit dem Broker registriert jedes Consumer-Team seinen Contract bei jedem Build, und das Provider-Team fragt beim Broker automatisch alle aktuell gültigen Contracts ab, statt sie fest im eigenen Repository zu pflegen.

Ein besonders nützliches Feature des Pact Brokers ist die sogenannte "can-i-deploy"-Prüfung: Bevor ein Provider deployt wird, fragt die CI-Pipeline den Broker, ob alle registrierten Consumer-Contracts für die aktuelle Provider-Version bereits erfolgreich verifiziert wurden. Ist das nicht der Fall, blockiert der Broker das Deployment, wodurch ein Breaking Change gar nicht erst in Produktion gelangt, unabhängig davon, ob jemand manuell daran gedacht hat, alle Consumer zu prüfen.

7. Contract Tests in die CI-Pipeline integrieren

Contract Tests gehören in zwei getrennte CI-Jobs: einen für Consumer-Tests, der bei jedem Build im Consumer-Repository läuft und den Contract beim Broker veröffentlicht, und einen für Provider-Verifikation, der bei jedem Build im Provider-Repository alle registrierten Contracts abruft und verifiziert. Diese Trennung erlaubt es beiden Teams, unabhängig voneinander zu deployen, solange die "can-i-deploy"-Prüfung grün bleibt.

Ein häufiges Missverständnis ist, Contract Testing als Ersatz für End-to-End-Tests zu betrachten. Contract Tests prüfen die Schnittstelle zwischen zwei Systemen, nicht die vollständige Geschäftslogik hinter der Schnittstelle. Eine kleine Anzahl echter End-to-End-Tests für die kritischsten Nutzerflüsse bleibt sinnvoll, ergänzt durch eine deutlich größere Zahl schneller Contract Tests, die jede Consumer-Provider-Beziehung einzeln absichern.

8. Typische Fehler beim Einführen von Contract Testing

Ein verbreiteter Fehler ist, Contract Testing vom Provider-Team allein einführen zu lassen, ohne die Consumer-Teams einzubeziehen. Da Contracts konsumentengetrieben sind, braucht jedes Consumer-Team die Verantwortung, seinen eigenen Contract zu pflegen und bei Änderungen an der eigenen Nutzung zu aktualisieren. Ohne diese geteilte Verantwortung verkommt Contract Testing zu einer weiteren Form der Schema-Validierung, die den eigentlichen Vorteil konsumentenspezifischer Erwartungen verschenkt.


<?php

// WRONG: provider team writes contracts on behalf of consumers,
// guessing what fields might be used — defeats the purpose of
// consumer driven contracts and drifts from real usage over time.

// RIGHT: each consumer team owns and maintains its own contract test,
// re-generating the contract whenever its actual API usage changes.

Ein zweiter Fehler ist, die Provider-Verifikation ohne echte Datenbank-Fixtures laufen zu lassen, die den im Contract erwarteten Zustand tatsächlich abbilden. Wird die Provider-Anwendung mit leeren oder falschen Testdaten gestartet, kann die Verifikation an einem 404 statt an einem echten Vertragsbruch scheitern, was das eigentliche Signal von Contract Testing verwässert und zu ignorierten, immer roten Tests führt.

9. Contract Testing im Vergleich zu anderen API-Teststrategien

Contract Testing ist eine von mehreren Strategien, um API-Kompatibilität abzusichern. Die folgende Tabelle ordnet die Ansätze nach Anwendungsfall ein.

Strategie Prüft Geschwindigkeit Deckt fremde Consumer ab
End-to-End-Test Vollständigen Nutzerfluss Langsam Nein
OpenAPI-Validierung Response gegen zentrales Schema Schnell Teilweise
Consumer Driven Contract (PACT) Explizite Consumer-Erwartungen Schnell Ja
Manuelles Cross-Team-Abstimmen Was jemand zufällig erwähnt Sehr langsam Unzuverlässig

Für Symfony APIs mit mehreren, teils externen Consumern liefert Contract Testing mit PACT die zuverlässigste Absicherung gegen Breaking Changes, während OpenAPI-Validierung eine gute erste Stufe für kleinere Projekte mit wenigen bekannten Consumern bleibt.

Mironsoft

Symfony APIs, Contract Testing und CI-Pipelines

Breaking Changes finden, bevor Consumer sie finden?

Wir führen Consumer Driven Contract Testing mit PACT in bestehende Symfony APIs ein, richten den Pact Broker samt "can-i-deploy"-Gate ein und verhindern so, dass API-Änderungen unbemerkt Consumer brechen.

Contract-Einführung

PACT-Setup für Consumer- und Provider-Tests in Symfony

Broker-Betrieb

Pact Broker Deployment und Deployment-Gate-Konfiguration

API-Governance

OpenAPI-Spezifikationen und Schema-Validierung als Ergänzung

10. Zusammenfassung

Contract Testing schließt eine Lücke, die klassische End-to-End-Tests strukturell nicht schließen können: die Sicherheit, dass eine Änderung an einer Symfony API alle tatsächlichen Consumer weiterhin bedient. Mit PACT als Consumer Driven Contract-Framework definiert jeder Consumer seine eigenen Erwartungen, das Provider-Team verifiziert alle registrierten Contracts gegen die echte Implementierung, und der Pact Broker verhindert über die "can-i-deploy"-Prüfung, dass ein Breaking Change überhaupt deployt wird.

Für kleinere Projekte mit wenigen, gut bekannten Consumern reicht oft eine leichtere OpenAPI-Schema-Validierung als erster Schritt. Für Symfony APIs mit mehreren Teams oder externen Partnern ist echtes Contract Testing die robustere Investition, weil es genau die konsumentenspezifischen Erwartungen abbildet, die eine zentrale Schema-Prüfung systematisch übersieht.

Contract Testing für Symfony APIs: Das Wichtigste auf einen Blick

Consumer Driven

Jeder Consumer definiert seinen eigenen Contract, nur genutzte Felder lösen Testfehlschläge aus.

Zwei getrennte Jobs

Consumer-Test veröffentlicht den Contract, Provider-Verifikation prüft ihn gegen die echte Anwendung.

Pact Broker als Gate

"can-i-deploy"-Prüfung blockiert Deployments, bevor alle Contracts verifiziert wurden.

Ergänzung, kein Ersatz

Eine kleine Zahl End-to-End-Tests bleibt für kritische Nutzerflüsse weiterhin sinnvoll.

11. FAQ: Contract Testing für Symfony APIs

1Was ist Contract Testing?
Jeder Consumer definiert seine tatsächlichen Erwartungen, die automatisch gegen den echten Provider verifiziert werden.
2Was bedeutet Consumer Driven?
Jedes Consumer-Team beschreibt seine tatsächliche Nutzung, nur genutzte Felder lösen Testfehlschläge aus.
3Wie erzeuge ich einen PACT-Contract?
Consumer-Test definiert erwartete Anfrage und Response gegen einen PACT-Mock-Server, der die Contract-Datei erzeugt.
4Wie funktioniert Provider-Verifikation?
Echte Anwendung mit passenden Fixtures starten, PACT spielt registrierte Contracts dagegen ab.
5Brauche ich einen Pact Broker?
Dringend empfohlen, sonst müssen Contract-Dateien manuell zwischen Repositories kopiert werden.
6Was macht can-i-deploy?
Blockiert Deployment, wenn nicht alle registrierten Contracts erfolgreich verifiziert wurden.
7OpenAPI-Validierung = Contract Testing?
Nein, prüft gegen zentrales Schema statt gegen individuelle Consumer-Erwartungen.
8Ersetzt es End-to-End-Tests?
Nein, eine kleine Zahl End-to-End-Tests für kritische Flüsse bleibt weiterhin sinnvoll.
9Wer pflegt den Contract?
Immer das Consumer-Team, sonst geht der eigentliche Vorteil des Ansatzes verloren.
10Haeufiger Fehler bei Verifikation?
Fehlende Fixtures lassen die Verifikation an einem 404 statt an einem echten Vertragsbruch scheitern.