Verbraucherverträge mit Pakt

Verbraucherverträge mit Pact

1. Überblick

In diesem kurzen Artikel befassen wir uns mit dem Konzept verbraucherorientierter Verträge.

Wir testen die Integration mit einem externen REST-Service über einen Vertrag, den wir mithilfe derPact-Bibliothek definieren. That contract can be defined by the client, then picked up by the provider and used for development of its services.

Wir erstellen auch vertragliche Tests für die Client- und Provider-Anwendungen.

2. Was istPact?

Using Pact, we can define consumer expectations for a given provider (that can be an HTTP REST service) in the form of a contract (daher der Name der Bibliothek).

Wir werden diesen Vertrag mit dem vonPact bereitgestellten DSL abschließen. Nach der Definition können wir die Interaktionen zwischen Verbrauchern und Anbietern mithilfe des Scheindienstes testen, der auf der Grundlage des definierten Vertrags erstellt wird. Außerdem testen wir den Service anhand des Vertrags mithilfe eines Scheinclients.

3. Maven-Abhängigkeit

Um zu beginnen, müssen wir die Maven-Abhängigkeit zur Bibliothek vonpact-jvm-consumer-junit_2.11hinzufügen:


    au.com.dius
    pact-jvm-consumer-junit_2.11
    3.5.0
    test

4. Vertrag definieren

Wenn wir einen Test mitPact erstellen möchten, müssen wir zuerst@Rule definieren, die in unserem Test verwendet werden:

@Rule
public PactProviderRuleMk2 mockProvider
  = new PactProviderRuleMk2("test_provider", "localhost", 8080, this);

Wir übergeben den Anbieternamen, den Host und den Port, an dem der Server-Mock (der aus dem Vertrag erstellt wird) gestartet wird.

Angenommen, der Dienst hat den Vertrag für zwei HTTP-Methoden definiert, die er verarbeiten kann.

Die erste Methode ist eine GET-Anforderung, die JSON mit zwei Feldern zurückgibt. Wenn die Anforderung erfolgreich ist, gibt sie einen 200-HTTP-Antwortcode und den Content-Type-Header für JSON zurück.

Definieren wir einen solchen Vertrag mitPact.

We need to use the @Pact annotation and pass the consumer name for which the contract is defined. Innerhalb der mit Anmerkungen versehenen Methode können wir unseren GET-Vertrag definieren:

@Pact(consumer = "test_consumer")
public RequestResponsePact createPact(PactDslWithProvider builder) {
    Map headers = new HashMap<>();
    headers.put("Content-Type", "application/json");

    return builder
      .given("test GET")
        .uponReceiving("GET REQUEST")
        .path("/pact")
        .method("GET")
      .willRespondWith()
        .status(200)
        .headers(headers)
        .body("{\"condition\": true, \"name\": \"tom\"}")
        (...)
}

Mit dem DSL vonPactdefinieren wir, dass wir für eine bestimmte GET-Anforderung eine 200-Antwort mit bestimmten Headern und Körpern zurückgeben möchten.

Der zweite Teil unseres Vertrages ist die POST-Methode. Wenn der Client eine POST-Anforderung mit einem geeigneten JSON-Body an den Pfad/pact sendet, gibt er einen 201-HTTP-Antwortcode zurück.

Definieren wir einen solchen Vertrag mitPact:

(...)
.given("test POST")
.uponReceiving("POST REQUEST")
  .method("POST")
  .headers(headers)
  .body("{\"name\": \"Michael\"}")
  .path("/pact")
.willRespondWith()
  .status(201)
.toPact();

Beachten Sie, dass wir am Ende des Vertrags die MethodetoPact() aufrufen müssen, um eine Instanz vonRequestResponsePact zurückzugeben.

4.1. Resultierendes Paktartefakt

Standardmäßig werden Paktdateien im Ordnertarget/pactsgeneriert. Um diesen Pfad anzupassen, können wirmaven-surefire-plugin: konfigurieren


    org.apache.maven.plugins
    maven-surefire-plugin
    
        
            target/mypacts
        
    
    ...

Der Maven-Build generiert eine Datei mit dem Namentest_consumer-test_provider.json im Ordnertarget/mypacts, die die Struktur der Anforderungen und Antworten enthält:

{
    "provider": {
        "name": "test_provider"
    },
    "consumer": {
        "name": "test_consumer"
    },
    "interactions": [
        {
            "description": "GET REQUEST",
            "request": {
                "method": "GET",
                "path": "/"
            },
            "response": {
                "status": 200,
                "headers": {
                    "Content-Type": "application/json"
                },
                "body": {
                    "condition": true,
                    "name": "tom"
                }
            },
            "providerStates": [
                {
                    "name": "test GET"
                }
            ]
        },
        {
            "description": "POST REQUEST",
            ...
        }
    ],
    "metadata": {
        "pact-specification": {
            "version": "3.0.0"
        },
        "pact-jvm": {
            "version": "3.5.0"
        }
    }
}

5. Testen des Kunden und des Anbieters anhand des Vertrags

Nachdem wir unseren Vertrag abgeschlossen haben, können wir Tests für den Kunden und den Anbieter erstellen.

Bei jedem dieser Tests wird ein Modell seines Gegenübers verwendet, das auf dem Vertrag basiert. Dies bedeutet:

  • Der Client verwendet einen Scheinanbieter

  • Der Anbieter verwendet einen Mock-Client

Tatsächlich werden die Tests gegen den Vertrag durchgeführt.

5.1. Client testen

Sobald wir den Vertrag definiert haben, können wir die Interaktionen mit dem Service testen, der basierend auf diesem Vertrag erstellt wird. We can create normal JUnit test but we need to remember to put the @PactVerification annotation at the beginning of the test.

Schreiben wir einen Test für die GET-Anfrage:

@Test
@PactVerification()
public void givenGet_whenSendRequest_shouldReturn200WithProperHeaderAndBody() {

    // when
    ResponseEntity response = new RestTemplate()
      .getForEntity(mockProvider.getUrl() + "/pact", String.class);

    // then
    assertThat(response.getStatusCode().value()).isEqualTo(200);
    assertThat(response.getHeaders().get("Content-Type").contains("application/json")).isTrue();
    assertThat(response.getBody()).contains("condition", "true", "name", "tom");
}

The @PactVerification annotation takes care of starting the HTTP service. Im Test müssen wir nur die GET-Anfrage senden und bestätigen, dass unsere Antwort dem Vertrag entspricht.

Fügen wir auch den Test für den POST-Methodenaufruf hinzu:

HttpHeaders httpHeaders = new HttpHeaders();
httpHeaders.setContentType(MediaType.APPLICATION_JSON);
String jsonBody = "{\"name\": \"Michael\"}";

// when
ResponseEntity postResponse = new RestTemplate()
  .exchange(
    mockProvider.getUrl() + "/create",
    HttpMethod.POST,
    new HttpEntity<>(jsonBody, httpHeaders),
    String.class
);

//then
assertThat(postResponse.getStatusCode().value()).isEqualTo(201);

Wie wir sehen können, ist der Antwortcode für die POST-Anforderung gleich 201 - genau so, wie er im Vertrag vonPactdefiniert wurde.

Da wir die Annotation@PactVerification()verwendet haben, startet die BibliothekPactden Webserver basierend auf dem zuvor definierten Vertrag vor unserem Testfall.

5.2. Testen des Anbieters

Der zweite Schritt unserer Vertragsüberprüfung besteht in der Erstellung eines Tests für den Anbieter mithilfe eines Scheinclients auf der Grundlage des Vertrags.

Unsere Provider-Implementierung wird durch diesen Vertrag auf TDD-Weise vorangetrieben.

In unserem Beispiel verwenden wir eine Spring Boot-REST-API.

Um unseren JUnit-Test zu erstellen, müssen wir zunächst die Abhängigkeit vonpact-jvm-provider-junit_2.11hinzufügen:


    au.com.dius
    pact-jvm-provider-junit_2.11
    3.5.0
    test

Auf diese Weise können wir einen JUnit-Test mitPactRunner erstellen und den Anbieternamen und den Speicherort des Pakt-Artefakts angeben:

@RunWith(PactRunner.class)
@Provider("test_provider")
@PactFolder("pacts")
public class PactProviderTest {
    //...
}

Damit diese Konfiguration funktioniert, müssen wir die Dateitest_consumer-test_provider.jsonim Ordnerpactsunseres REST-Serviceprojekts ablegen.

Als Nächstes definieren wir das Ziel, das zum Überprüfen der Interaktionen im Vertrag verwendet werden soll, und starten die Spring Boot-App, bevor die Tests ausgeführt werden:

@TestTarget
public final Target target = new HttpTarget("http", "localhost", 8082, "/spring-rest");

private static ConfigurableWebApplicationContext application;

@BeforeClass
public static void start() {
    application = (ConfigurableWebApplicationContext)
      SpringApplication.run(MainApplication.class);
}

Schließlich geben wir die Zustände im Vertrag an, die wir testen möchten:

@State("test GET")
public void toGetState() { }

@State("test POST")
public void toPostState() { }

Wenn Sie diese JUnit-Klasse ausführen, werden zwei Tests für die beiden GET- und POST-Anforderungen ausgeführt. Werfen wir einen Blick auf das Protokoll:

Verifying a pact between test_consumer and test_provider
  Given test GET
  GET REQUEST
    returns a response which
      has status code 200 (OK)
      includes headers
        "Content-Type" with value "application/json" (OK)
      has a matching body (OK)

Verifying a pact between test_consumer and test_provider
  Given test POST
  POST REQUEST
    returns a response which
      has status code 201 (OK)
      has a matching body (OK)

Beachten Sie, dass wir den Code zum Erstellen eines REST-Service hier nicht angegeben haben. Der vollständige Service und Test finden Sie inGitHub project.

6. Fazit

In diesem kurzen Tutorial haben wir uns Consumer Driven Contracts angesehen.

Wir haben einen Vertrag mit der BibliothekPacterstellt. Nachdem wir den Vertrag definiert hatten, konnten wir den Kunden und die Dienstleistung anhand des Vertrags testen und versichern, dass sie der Spezifikation entsprechen.

Die Implementierung all dieser Beispiele und Codefragmente finden Sie inGitHub project - dies ist ein Maven-Projekt, daher sollte es einfach zu importieren und auszuführen sein, wie es ist.