In diesem Artikel werden wir über die Probleme sprechen, die durch Consumer Driven Contracts gelöst werden, und am Beispiel von Pact with Node.js und Spring Boot zeigen, wie diese angewendet werden. Und sprechen Sie über die Grenzen dieses Ansatzes.
Problem
Beim Testen von Produkten werden häufig Szenariotests verwendet, bei denen die Integration verschiedener Systemkomponenten in eine speziell ausgewählte Umgebung überprüft wird. Solche Tests auf Live-Diensten liefern das zuverlässigste Ergebnis (ohne Tests im Kampf). Gleichzeitig gehören sie zu den teuersten.
- Es wird oft fälschlicherweise angenommen, dass die Integrationsumgebung nicht fehlertolerant sein sollte. SLA, Garantien für solche Umgebungen werden selten ausgesprochen, aber wenn sie nicht verfügbar sind, müssen die Teams entweder die Veröffentlichung verzögern oder auf das Beste hoffen und ohne Tests in den Kampf ziehen. Obwohl jeder weiß, dass Hoffnung keine Strategie ist . Und neue Infrastrukturtechnologien erschweren nur die Arbeit mit Integrationsumgebungen.
- Ein weiterer Schmerz ist die Arbeit mit Testdaten . Viele Szenarien erfordern einen bestimmten Status des Systems, Fixtures. Wie nah sollten sie an der Bekämpfung von Daten sein? Wie kann man sie vor dem Test auf den neuesten Stand bringen und nach Abschluss reinigen?
- Tests sind zu instabil . Und das nicht nur wegen der Infrastruktur, die wir im ersten Absatz erwähnt haben. Der Test kann fehlschlagen, weil ein benachbartes Team eigene Überprüfungen gestartet hat, die den erwarteten Status des Systems verletzt haben! Viele falsch negative Schecks, schuppige Tests beenden ihr Leben bei
@Ignored
. Außerdem können verschiedene Teile der Integration von verschiedenen Teams unterstützt werden. Sie haben einen neuen Release-Kandidaten mit Fehlern eingeführt - sie haben alle Verbraucher gebrochen. Jemand löst dieses Problem mit dedizierten Testschleifen. Aber auf Kosten der Multiplikation der Supportkosten. - Solche Tests nehmen viel Zeit in Anspruch . Selbst mit Blick auf die Automatisierung können Ergebnisse für Stunden erwartet werden.
- Und um das Ganze abzurunden, wenn der Test wirklich richtig war, ist es bei weitem nicht immer möglich, die Ursache des Problems sofort zu finden. Es kann sich tief hinter Integrationsebenen verstecken. Oder es kann das Ergebnis einer unerwarteten Kombination von Zuständen vieler Systemkomponenten sein.
Stabile Tests in einer Integrationsumgebung erfordern ernsthafte Investitionen von QS, Entwicklern und sogar Ops. Kein Wunder, dass sie ganz oben auf der
Testpyramide stehen . Solche Tests sind nützlich, aber die Ressourcenökonomie erlaubt es ihnen nicht, alles zu überprüfen. Die Hauptquelle ihres Wertes ist die Umwelt.
Unterhalb derselben Pyramide befinden sich weitere Tests, bei denen wir das Vertrauen gegen kleinere Unterstützungskopfschmerzen austauschen - unter Verwendung von Isolationsprüfungen. Je körniger, desto kleiner der Maßstab des Tests, desto geringer ist die Abhängigkeit von der äußeren Umgebung. Ganz unten in der Pyramide befinden sich Unit-Tests. Wir überprüfen einzelne Funktionen, Klassen, arbeiten weniger mit Geschäftssemantik als vielmehr mit Konstruktionen einer bestimmten Implementierung. Diese Tests geben schnelles Feedback.
Aber sobald wir die Pyramide hinuntergehen, müssen wir die Umwelt durch etwas ersetzen. Stubs erscheinen - als ganze Dienste und einzelne Entitäten der Programmiersprache. Mit Hilfe von Steckern können wir Komponenten isoliert testen. Sie verringern aber auch die Gültigkeit der Prüfungen. Wie kann sichergestellt werden, dass der Stub die richtigen Daten zurückgibt? Wie kann man seine Qualität sicherstellen?
Die Lösung kann eine umfassende Dokumentation sein , die verschiedene Szenarien und mögliche Zustände von Systemkomponenten beschreibt. Formulierungen lassen jedoch immer noch Interpretationsfreiheit. Daher ist eine gute Dokumentation ein lebendiges Artefakt, das sich ständig verbessert, wenn das Team den Problembereich versteht. Wie kann dann die Einhaltung der Dokumentationsstubs sichergestellt werden?
Bei vielen Projekten können Sie eine Situation beobachten, in der die Stubs von denselben Leuten geschrieben wurden, die das Testartefakt entwickelt haben. Beispielsweise erstellen Entwickler mobiler Anwendungen selbst Stubs für ihre Tests. Infolgedessen können Programmierer die Dokumentation auf ihre eigene Weise verstehen (was völlig normal ist), sie machen den Stub mit dem falsch erwarteten Verhalten, schreiben den Code entsprechend (mit grünen Tests) und Fehler treten während der realen Integration auf.
Darüber hinaus wird die Dokumentation normalerweise nachgeschaltet - Clients verwenden Dienstspezifikationen (in diesem Fall kann ein anderer Dienst ein Client des Dienstes sein). Es wird nicht ausgedrückt,
wie Verbraucher Daten verwenden, welche Daten überhaupt benötigt werden und welche Annahmen sie für diese Daten treffen. Die Folge dieser Unwissenheit ist das
Gesetz von Hyrum .
Hyrum Wright hat lange Zeit öffentliche Tools in Google entwickelt und beobachtet, wie kleinste Änderungen zu Störungen bei Kunden führen können, die die impliziten (undokumentierten) Funktionen seiner Bibliotheken verwendet haben. Eine solche versteckte Konnektivität erschwert die Entwicklung der API.
Diese Probleme können bis zu einem gewissen Grad mithilfe von Verbraucherverträgen gelöst werden. Wie jeder Ansatz und jedes Tool weist es eine Reihe von Anwendbarkeit und Kosten auf, die wir ebenfalls berücksichtigen werden. Die Umsetzung dieses Ansatzes hat einen ausreichenden Reifegrad erreicht, um ihre Projekte auszuprobieren.
Was ist eine CDC?
Drei Schlüsselelemente:
- Der Vertrag . Beschrieben mit etwas DSL, implementierungsabhängig. Es enthält eine Beschreibung der API in Form von Interaktionsszenarien: Wenn eine bestimmte Anforderung eingeht, sollte der Client eine bestimmte Antwort erhalten.
- Kundentests . Darüber hinaus verwenden sie einen Stub, der automatisch aus dem Vertrag generiert wird.
- Tests für die API . Sie werden auch aus dem Vertrag generiert.
Somit ist der Vertrag ausführbar. Das Hauptmerkmal des Ansatzes besteht darin, dass die Anforderungen für das Verhalten der API vom Client zum Server
vorgelagert werden.
Der Vertrag konzentriert sich auf das Verhalten,
das für den Verbraucher
wirklich wichtig ist. Macht seine Annahmen über die API explizit.
Das Hauptziel der CDC ist es, ihren Entwicklern und den Entwicklern ihrer Kunden ein Verständnis des API-Verhaltens zu vermitteln. Dieser Ansatz lässt sich gut mit BDD kombinieren. Bei
Besprechungen mit drei Amigo können Sie die Lücken für den Vertrag skizzieren. Letztendlich dient dieser Vertrag auch zur Verbesserung der Kommunikation; ein gemeinsames Verständnis des Problembereichs zu teilen und die Lösung innerhalb und zwischen Teams umzusetzen.
Pakt
Betrachten Sie die Verwendung von CDC als Beispiel aus Pact, einer seiner Implementierungen. Angenommen, wir erstellen eine Webanwendung für Konferenzteilnehmer. In der nächsten Iteration entwickelt das Team einen Präsentationsplan - bisher ohne Geschichten wie Abstimmungen oder Notizen, nur die Ausgabe des Berichtsrasters. Der Quellcode für das Beispiel ist
hier .
Bei einem Treffen von
drei vier Amigo treffen sich ein Produkt, ein Tester, Entwickler des Backends und eine mobile Anwendung. Das sagen sie
- In der Benutzeroberfläche wird eine Liste mit dem Text angezeigt: Berichtstitel + Sprecher + Datum und Uhrzeit.
- Dazu muss das Backend Daten wie im folgenden Beispiel zurückgeben.
{ "talks":[ { "title":" ", "speakers":[ { "name":" " } ], "time":"2019-05-27T12:00:00+03:00" } ] }
Danach schreibt der Frontend-Entwickler den Client-Code (Backend für Frontend). Er installiert eine Paktvertragsbibliothek im Projekt:
yarn add --dev @pact-foundation/pact
Und beginnt einen Test zu schreiben. Es konfiguriert den lokalen Stub-Server, der den Dienst mit Berichtszeitplänen simuliert:
const provider = new Pact({
Der Vertrag ist eine JSON-Datei, die die Szenarien beschreibt, in denen der Client mit dem Service interagiert. Sie müssen es jedoch nicht manuell beschreiben, da es aus den Einstellungen des Stubs im Code gebildet wird. Der Entwickler vor dem Test beschreibt das folgende Verhalten.
provider.setup().then(() => provider .addInteraction({ uponReceiving: "a request for schedule", withRequest: { method: "GET", path: "/schedule" }, willRespondWith: { status: 200, headers: { "Content-Type": "application/json;charset=UTF-8" }, body: { talks: [ { title: " ", speakers: [ { name: " " } ], time: "2019-05-27T12:00:00+03:00" } ] } } }) .then(() => done()) );
In diesem Beispiel haben wir die spezifische erwartete Serviceanforderung angegeben, aber pact-js unterstützt auch
verschiedene Methoden zum Ermitteln von Übereinstimmungen .
Schließlich schreibt der Programmierer einen Test des Teils des Codes, der diesen Stub verwendet. Im folgenden Beispiel werden wir es der Einfachheit halber direkt nennen.
it("fetches schedule", done => { fetch(`http://localhost:${pactServerPort}/schedule`) .then(response => response.json()) .then(json => expect(json).toStrictEqual({ talks: [ { title: " ", speakers: [ { name: " " } ], time: "2019-05-27T12:00:00+03:00" } ] })) .then(() => done()); });
In einem realen Projekt kann dies entweder ein schneller Komponententest einer separaten Antwortinterpretationsfunktion oder ein langsamer UI-Test zum Anzeigen von Daten sein, die von einem Dienst empfangen wurden.
Während des Testlaufs überprüft pact, ob der Stub die in den Tests angegebene Anforderung erhalten hat. Die Abweichungen können in der Datei pact.log als unterschiedlich angesehen werden.
E, [2019-05-21T01:01:55.810194 #78394] ERROR -- : Diff with interaction: "a request for schedule" Diff -------------------------------------- Key: - is expected + is actual Matching keys and values are not shown { "headers": { - "Accept": "application/json" + "Accept": "*/*" } } Description of differences -------------------------------------- * Expected "application/json" but got "*/*" at $.headers.Accept
Wenn der Test erfolgreich ist, wird ein Vertrag im JSON-Format generiert. Es beschreibt das erwartete Verhalten der API.
{ "consumer": { "name": "schedule-consumer" }, "provider": { "name": "schedule-producer" }, "interactions": [ { "description": "a request for schedule", "request": { "method": "GET", "path": "/schedule", "headers": { "Accept": "application/json" } }, "response": { "status": 200, "headers": { "Content-Type": "application/json;charset=UTF-8" }, "body": { "talks":[ { "title":" ", "speakers":[ { "name":" " } ], "time":"2019-05-27T12:00:00+03:00" } ] }}} ], "metadata": { "pactSpecification": { "version": "2.0.0" } } }
Er gibt diesen Vertrag an den Backend-Entwickler. Angenommen, die API befindet sich im Spring Boot. Pact verfügt über eine
pact-jvm-provider-spring- Bibliothek, die mit MockMVC zusammenarbeiten kann. Aber wir werden uns den Spring Cloud-Vertrag ansehen, der CDC im Spring-Ökosystem implementiert. Es verwendet ein eigenes Vertragsformat, verfügt jedoch auch über einen Erweiterungspunkt zum Verbinden von Konvertern aus anderen Formaten. Das native Vertragsformat wird nur vom Spring Cloud-Vertrag selbst unterstützt - im Gegensatz zu Pact, das Bibliotheken für JVM, Ruby, JS, Go, Python usw. enthält.
Angenommen, in unserem Beispiel verwendet der Backend-Entwickler Gradle, um den Service zu erstellen. Es verbindet die folgenden Abhängigkeiten:
buildscript { // ... dependencies { classpath "org.springframework.cloud:spring-cloud-contract-pact:2.1.1.RELEASE" } } plugins { id "org.springframework.cloud.contract" version "2.1.1.RELEASE" // ... } // ... dependencies { // ... testImplementation 'org.springframework.cloud:spring-cloud-starter-contract-verifier' }
Und es legt den vom Frotender erhaltenen Paktvertrag in das Verzeichnis
src/test/resources/contracts
.
Standardmäßig subtrahiert das Spring-Cloud-Vertrags-Plugin Verträge davon. Während der Montage wird die Gradle-Task generateContractTests ausgeführt, die den folgenden Test im Verzeichnis build / generate-test-sources generiert.
public class ContractVerifierTest extends ContractsBaseTest { @Test public void validate_aggregator_client_aggregator_service() throws Exception {
Beim Starten dieses Tests wird ein Fehler angezeigt:
java.lang.IllegalStateException: You haven't configured a MockMVC instance. You can do this statically
Da wir verschiedene Tools zum Testen verwenden können, müssen wir dem Plug-In mitteilen, welches wir konfiguriert haben. Dies erfolgt über die Basisklasse, die die aus den Verträgen generierten Tests erbt.
public abstract class ContractsBaseTest { private ScheduleController scheduleController = new ScheduleController(); @Before public void setup() { RestAssuredMockMvc.standaloneSetup(scheduleController); } }
Um diese Basisklasse während der Generierung zu verwenden, müssen Sie das Spring-Cloud-Contract-Gradle-Plugin konfigurieren.
contracts { baseClassForTests = 'ru.example.schedule.ContractsBaseTest' }
Jetzt haben wir folgenden Test generiert:
public class ContractVerifierTest extends ContractsBaseTest { @Test public void validate_aggregator_client_aggregator_service() throws Exception {
Der Test wird erfolgreich gestartet, schlägt jedoch mit einem Überprüfungsfehler fehl. Der Entwickler hat die Implementierung des Dienstes noch nicht geschrieben. Aber jetzt kann er es auf der Grundlage eines Vertrags tun. Er kann sicherstellen, dass er die Anfrage des Kunden bearbeiten und die erwartete Antwort zurückgeben kann.
Der Serviceentwickler weiß durch den Vertrag, was er tun muss und welches Verhalten er implementieren muss.
Der Pakt kann tiefer in den Entwicklungsprozess integriert werden. Sie können einen Pact-Broker bereitstellen, der solche Verträge aggregiert, deren Versionierung unterstützt und ein Abhängigkeitsdiagramm anzeigt.

Das Hochladen eines neu generierten Vertrags auf den Broker kann in Schritt CI beim Erstellen des Clients erfolgen. Und geben Sie im Servercode das dynamische Laden des Vertrags per URL an. Spring Cloud Contract unterstützt dies ebenfalls.
CDC-Anwendbarkeit
Was sind die Einschränkungen von Verbraucherverträgen?
Für diesen Ansatz müssen
Sie mit zusätzlichen Tools wie Pakt
bezahlen . Verträge an sich sind ein zusätzliches Artefakt, eine weitere Abstraktion, die sorgfältig gepflegt und bewusst auf technische Praktiken angewendet werden muss.
Sie ersetzen keine e2e-Tests , da Stubs immer noch Stubs bleiben - Modelle realer Systemkomponenten, die zwar ein wenig sind, aber nicht der Realität entsprechen. Durch sie können komplexe Szenarien nicht verifiziert werden.
CDCs ersetzen auch keine API-Funktionstests . Ihre Unterstützung ist teurer als bei einfachen alten Komponententests. Paktentwickler empfehlen die Verwendung der folgenden Heuristiken: Wenn Sie den Vertrag entfernen und dies keine Fehler oder Fehlinterpretationen durch den Kunden verursacht, ist dies nicht erforderlich. Beispielsweise ist es nicht erforderlich, absolut alle API-Fehlercodes durch einen Vertrag zu beschreiben, wenn der Client sie auf die gleiche Weise verarbeitet. Mit anderen Worten, der Vertrag beschreibt für die Dienstleistung
nur das, was für den Kunden wichtig ist . Nicht mehr, aber nicht weniger.
Zu viele Verträge erschweren auch die Entwicklung der API.
Jeder zusätzliche Vertrag ist Anlass für rote Tests . Es ist notwendig, eine CDC so zu gestalten, dass jeder Fehlertest eine nützliche semantische Last trägt, die die Kosten ihrer Unterstützung überwiegt. Wenn der Vertrag beispielsweise die Mindestlänge eines bestimmten Textfelds
festlegt , die für den Verbraucher gleichgültig ist (er verwendet die
Toleran Reader- Technik), wird jede Änderung dieses Mindestwerts den Vertrag und die Nerven seiner Umgebung
zerstören . Eine solche Prüfung muss auf die Ebene der API selbst übertragen und abhängig von der Quelle der Einschränkungen implementiert werden.
Fazit
CDC verbessert die Produktqualität durch explizite Beschreibung des Integrationsverhaltens. Es hilft Kunden und Serviceentwicklern, ein gemeinsames Verständnis zu erreichen, und ermöglicht es Ihnen, über Code zu sprechen. Dies geht jedoch zu Lasten des Hinzufügens von Tools, der Einführung neuer Abstraktionen und zusätzlicher Aktionen von Teammitgliedern.
Gleichzeitig werden CDC-Tools und Frameworks aktiv entwickelt und sind bereits ausgereift, um Ihre Projekte zu testen. Test :)
Auf der QualityConf- Konferenz vom 27. bis 28. Mai wird Andrei Markelov über Testtechniken für Produkte sprechen, und Arthur Khineltsev wird über die Überwachung eines hoch geladenen Frontends sprechen, wenn der Preis für selbst einen kleinen Fehler Zehntausende trauriger Benutzer beträgt.
Kommen Sie und chatten Sie für Qualität!