Implementierung von Consumer-Driven Contract Testing mit Pact Broker und GitLab CI - DOAG
←
→
Transkription von Seiteninhalten
Wenn Ihr Browser die Seite nicht korrekt rendert, bitte, lesen Sie den Inhalt der Seite unten
Implementierung von Consumer- Driven Contract Testing mit Pact Broker und GitLab CI Frank Rosner und Raffael Stein, codecentric AG In diesem Artikel wird Consumer-Driven Contract Ein Consumer („Konsument“) ist definiert als eine Systemkompo- Testing als Alternative zu End-to-End-Tests nente, die Daten oder Funktionalität eines anderen Dienstes ver- wendet. Komponenten, die Daten oder Funktionalität bereitstellen, vorgestellt, um die Komponenten eines verteilten werden dementsprechend als Provider („Anbieter“) bezeichnet. Systems entkoppelt zu testen. Pact ist der De-facto- Streng genommen handelt es sich bei Consumer und Provider um Standard für Contract Testing. Es beinhaltet eine Rollen, denn eine Softwarekomponente kann selbstverständlich sowohl Funktionalität anderer Komponenten konsumieren als auch programmiersprachenunabhängige Spezifikation, selbst welche anbieten. um Interaktionen zwischen Diensten in Form von JSON-Dokumenten abzubilden. Der Pact Broker Die Pact-Spezifikation [1] definiert ein Format, um Interaktionen kann in Kombination mit anderen Werkzeugen, wie als JSON-Dokumente abzubilden. Dieses Format ist programmier- sprachenunabhängig und es existieren Implementierungen in Ruby, zum Beispiel GitLab CI, eingesetzt werden, um den JavaScript, Go, Python, Swift, PHP sowie für JVM- und .NET-Spra- Entwicklungsprozess zu unterstützen. chen. Im Java-Universum wird üblicherweise pact-jvm verwendet. C onsumer-Driven Contract Testing (zu Deutsch konsumenten- getriebenes Vertragstesten) ist eine Alternative zu End-to- End-Tests, bei der nicht alle Dienste zur gleichen Zeit bereitgestellt Der Pact Broker ist ein Werkzeug, das beim Zusammenstecken eines Pact-Workflows unterstützt. In diesem Artikel wollen wir aufzeigen, wie Pact theoretisch funktioniert und wie es praktisch eingesetzt sein müssen. Contract Testing ermöglicht es, die Komponenten ei- werden kann. Dabei gehen wir im Speziellen darauf ein, wie man nes verteilten Systems entkoppelt zu testen, indem Interaktionen in mithilfe des Pact Broker und GitLab CI einen teilautomatisierten, Consumer- und Provider-Tests aufgeteilt werden, die jeweils unab- konsumentengetriebenen Entwicklungsprozess realisieren kann. hängig voneinander ausgeführt werden können. Der Rest dieses Artikels ist wie folgt aufgebaut: Zunächst werden Pact ist der De-facto-Standard für Consumer-Driven Contract Tes- die theoretischen Grundlagen von Pact vermittelt. Anschließend ting. Es wird meist verwendet, um Request-Response-Interak- wird die Funktionalität des Pact Broker näher beleuchtet. Der da- tionen zu testen, zum Beispiel die Kommunikation zwischen zwei rauffolgende Abschnitt erklärt am Beispiel von GitLab CI, wie man Diensten auf Grundlage des HTTP-Protokolls. Pact beinhaltet je- den Pact Broker in die eigene CI-Landschaft integrieren kann. Wir doch auch eine Spezifikation für asynchrone Interaktionen auf Basis diskutieren zum Abschluss noch Vor- und Nachteile von Pact und von Ereignissen beziehungsweise Nachrichten. fassen die wichtigsten Punkte zusammen. 62 iii iii iii www.ijug.eu iii
Konzepte des Consumer-Driven Der Consumer-Workflow Contract Testings mit Pact Nachdem wir die grundlegenden Konzepte von Pact kennengelernt Der Workflow des Consumer-Driven Contract Testing beinhaltet haben, wollen wir uns nun den Entwicklungsworkflow der Con- verschiedene Entitäten und Konzepte. In den nächsten Abschnit- sumer näher ansehen. Nehmen wir an, dass ein Consumer eine ten wollen wir uns die Grundlagen diesbezüglich anschauen, bevor HTTP-Schnittstelle eines anderen Dienstes nutzen möchte. Somit wir den Workflow aus Sicht des Entwickelnden einmal genauer be- ist der erste Schritt, die benötigten Interactions in einem Pact-File leuchten. Zu Anschauungszwecken wird in diesem Artikel immer zu definieren. wieder auf das folgende Beispielszenario verwiesen: Obwohl es grundsätzlich möglich ist, Pact-Files mit einem Textedi- Man stelle sich eine Webanwendung vor, die Login-Funktionalität tor zu bearbeiten, sollten stattdessen besser Consumer-Tests ge- bereitstellt. Das Frontend wird als JavaScript-Anwendung entwi- schrieben werden. Die Consumer-Tests verifizieren nicht nur den ckelt, wohingegen das Backend in Kotlin umgesetzt wird. korrekten Aufruf des Providers aus Sicht des Consumers, sondern generieren zusätzlich das Pact-File, in dem alle getesteten Interac- Die folgenden grundlegenden Konzepte sind nun in Bezug auf den tions enthalten sind. Einsatz von Pact in diesem Szenario relevant: Im nächsten Schritt werden die Provider-Tests gegen das generierte • Consumer – Eine Anwendung nimmt die Rolle eines Consumers Pact-File ausgeführt. Eine erfolgreiche Verification zeigt an, dass die ein, sobald sie Funktionalität einer anderen Komponente ver- Consumer-Version, die den Vertrag generiert hat, mit der Provider- wendet. Sie kann beispielsweise einen HTTP-Request an einen Version, die ihn verifiziert hat, kompatibel ist. Stellt man beide Ver- Webservice schicken. In unserem Beispiel wäre die JavaScript- sionen gemeinsam in Produktion bereit, sollte das Zusammenspiel Anwendung ein Consumer der Login-Funktionalität. der Komponenten wie erwartet funktionieren. • Provider – Im Gegenzug beinhaltet die Providerrolle das Anbie- Der Provider-Workflow ten von Funktionalität, beispielsweise einer HTTP-Schnittstelle. Obwohl Pact konsumentengetrieben ist, bietet es auch für die Wei- Bezogen auf unser Beispiel wäre das Backend ein Provider der terentwicklung von Providern Vorteile. Will man beispielsweise ein Login-Funktionalität. API anpassen und dabei sicherstellen, dass alle existierenden Auf- rufe weiterhin funktionieren, dann genügt das Verifizieren aller vor- • Interaction – Eine Interaction definiert, welche Funktionalität handenen Verträge. wie konsumiert wird. Eine HTTP-Interaction beispielsweise be- inhaltet den Request vom Consumer zum Provider, den Provi- Ist die Verification erfolgreich, können die Änderungen ohne Anpas- derzustand zu dieser Zeit, sowie die Antwort des Providers. So sungen an den Consumer ausgerollt werden. Somit können Provider könnten wir einen erfolgreichen Login-Versuch als eine Interac- nicht nur neue Funktionalität hinzufügen, sondern auch ohne Risiko tion modellieren. veraltete Funktionalität entfernen. • Providerzustand – Der Providerzustand drückt den Zustand aus, Implementierung von Consumer-Tests in dem sich der Provider während der Interaction befindet. Dieser Ein Consumer-Test ist im Allgemeinen wie folgt aufgebaut: Zu- Zustand fungiert als eine Text Fixture in den Providertests, die es nächst wird die Interaktion definiert und entsprechend registriert. dem Entwickelnden erlauben, Mocks oder Datenbanken entspre- Die Pact-Bibliothek generiert dann die Pact-Files und erstellt einen chend zu konfigurieren. Für unser Login-Szenario ist ein Zustand Stub-Server, der den echten Provider imitiert. Anschließend kann vorstellbar, in dem definiert ist, dass eine Benutzerin „Erika Mus- man die Consumer-Logik ausführen, die das API anspricht. Der Test terfrau“ existiert und ein spezifiziertes Passwort besitzt. ist erfolgreich, wenn die getätigte Anfrage der in der Interaktion de- finierten Anfrage entspricht. • Vertrag/Pact-File – Der Vertrag, auch Pact-File genannt, beinhal- tet alle Interactions in einem konkreten Consumer-Provider-Paar. Listing 1 zeigt einen in JavaScript entwickelten Consumer-Test für Zwischen unserem Frontend und Backend gäbe es demnach einen den Login-Endpunkt. Wir verwenden hierbei die Pact-Implementie- Vertrag, der alle Interactions in Bezug auf Login und Logout enthält. rung pact-js und jest als Testing-Framework. • Verification – Während der Verification des Vertrags werden die Zunächst wird der Provider konfiguriert. Die Providerkonfiguration darin enthaltenen Interactions gegen den Providercode simu- enthält die Namen des Consumers und des Providers sowie zusätz- liert. Anschließend werden die tatsächlichen Antworten mit den liche Optionen für den Stub-Server, wie beispielsweise den Port. im Vertrag definierten Antworten verglichen. Das Ergebnis sollte Anschließend definieren wir die Interaction: Gegeben sei ein Benut- in irgendeiner Art an den Entwickelnden des Consumers kommu- zender mit gültigen Zugangsdaten. Werden diese Zugangsdaten an niziert werden. den Endpunkt gesendet, so erhalten wir eine Antwort mit HTTP- Status-Code 200. Es sei an dieser Stelle nochmals darauf hingewiesen, dass eine An- weisung sowohl Consumer als auch Provider sein kann. Frontends Indem wir diese Interaktion zum Provider hinzufügen, teilen wir sind zwar typischerweise ausschließlich Consumer, aber auch da ist dem Stub-Server mit, wie er auf eine Login-Anfrage reagieren soll. eine bidirektionale Kommunikation vorstellbar, beispielsweise bei Wie man das API dann tatsächlich aufruft und welche Assertions der Verwendung von WebSockets. man zusätzlich schreibt, ist an dieser Stelle nicht vorgeschrieben. Java aktuell 01/21 63
In unserem Beispiel überprüfen wir nur, dass wir Status-Code 200 import { Interaction, Pact } from '@pact-foundation/pact'; zurückerhalten, wenn wir den UserService mit den entsprechenden const provider = new Pact(providerConfig); Zugangsdaten aufrufen. const successfulLogin = new Interaction() .given('jane.doe has password 123456') In einer echten Anwendung werden die Interaktionen wahrschein- .uponReceiving('username jane.doe and password 123456') lich wesentlich komplexer aussehen. Neben einem umfangreicheren .withRequest({ Datenmodell sind in den meisten Fällen auch HTTP-Header von Be- method: 'POST', path: '/login', deutung. Ein weiteres nützliches Feature sind Matcher [2]. Mithilfe body: { von Matchern können Interaktionen so definiert werden, dass die username: "jane.doe", password: "123456" echte Antwort nicht exakt mit der definierten Antwort übereinstim- } men muss. }) .willRespondWith({ status: 200 Austausch von Pact-Files }); Nachdem ein Consumer ein neues Pact-File generiert hat, muss await provider.addInteraction(successfulLogin); es mit allen betroffenen Providern geteilt werden, damit diese die Interactions verifizieren können. Grundsätzlich kann dies auf zwei const response = await UserService.login({ username: "jane.doe", Arten geschehen: password: "123456" }); 1. Committen der Pact-Files in die Quelltext-Repositories der Pro- expect(response.status).toBe(200); vider. Die einfachste Variante dieses Workflows ist, manuell ei- nen Merge-Request beim Provider zu erstellen, der die geänder- Listing 1: Consumer-Pact-Test in JavaScript unter Verwendung von ten Pact-Files enthält. In diesem Zuge kann die Build-Pipeline pact-js und jest die Verification-Tests ausführen. Das Erstellen eines Merge-Re- quests lässt sich selbstverständlich auch automatisieren. ment übergeben. Dieser wird benötigt, um zu entscheiden, welche 2. Herunterladen der Pact-Files durch die Provider. Anstatt die Interaktionen für den Test relevant sind. Pact-Files in die Provider-Repositories zu duplizieren, kann ein Consumer seine Pact-Files an einen Drittanbieter schicken. Die @PactBroker-Annotation sorgt dafür, dass pact-jvm das Pact- Provider können diese dann von dort herunterladen und veri- File vom Pact Broker bezieht. Wenn die Pact-Files vom Dateisystem fizieren. Gebräuchliche Formen von Drittanbietern sind bei- gelesen werden sollen, so kann stattdessen die @PactFolder-An- spielsweise Build-Server-Artefakte (zum Beispiel GitLab-Build- notation verwendet werden. Artefakte), ein Objektspeicher (beispielsweise Amazon S3) oder der Pact Broker. Dank einer @TestTemplate-Methode, die mit einem Pact- VerificationInvocationContextProvider erweitert wird, weiß Das Einführen des Pact Broker hat den weiteren Vorteil, dass ein JUnit 5, dass es eine Testmethode für jede Interaktion erzeugen Provider die Ergebnisse der Verification ebenfalls an den Broker soll. In unserem Beispiel erzeugen wir eine neue Instanz unseres schicken kann. Somit können Dienste beim Broker anfragen, ob für AccountService, der auf HTTP-Anfragen wartet. Der Aufruf von eine Kombination aus Komponentenversionen bereits eine erfolg- pactContext.verifyInteraction() sorgt dafür, dass Pact die reiche Verification vorliegt. Ist dies der Fall, kann ein Deployment Interaktionen abspielt und die tatsächlichen Antworten mit den im dieser Versionen ohne Probleme durchgeführt werden. Vertrag definierten Antworten abgleicht. Nachdem wir nun die verfügbaren Möglichkeiten dafür kennenge- Bevor eine Interaktion abgespielt wird, prüft pact-jvm, in welchem lernt haben, wie man Pact-Files zwischen Consumern und Provi- Zustand sich der Provider laut Vertrag befinden soll und führt die dern austauschen kann, wollen wir uns nun der Implementierung entsprechenden @State-Methoden aus. In diesen Methoden kön- von Providertests widmen. nen beispielsweise Mocks konfiguriert werden. In unserem Fall de- finieren wir, dass der gemockte AuthenticationProvider die Zu- Implementierung von Providertests gangsdaten aus der Interaktion akzeptieren soll. Um einen Vertrag zu verifizieren, spielt das Pact-Framework alle In- teraktionen im Rahmen eines Providertests ab und imitiert somit ei- Nachdem alle Interaktionen verifiziert wurden, veröffentlicht pact- nen Consumer. Provider-Tests können in einer anderen Programmier- jvm die Ergebnisse, zum Beispiel an den Pact Broker, wenn es ent- sprache implementiert sein als Consumer-Tests. In unserem Beispiel sprechend konfiguriert ist. Wenn der Providertest fehlschlägt, dann werden wir Kotlin mit JUnit 5, pact-jvm und mockk verwenden. muss gegebenenfalls der Vertrag angepasst oder die Funktionali- tät des Providers erweitert werden, sodass der neue Vertrag erfüllt Listing 2 enthält alle grundlegenden Konzepte, die für einen Provi- werden kann. dertest notwendig sind, und illustriert dies am Beispiel des Vertra- ges zwischen dem Account-Service und der UI. Einsatzzweck des Pact Broker In den vorigen Abschnitten des Artikels haben wir gesehen, dass Die Klassenannotation @Provider gibt an, dass es sich um einen der Austausch von Pact-Files und Verification-Ergebnissen einen Provider-Test handelt, und bekommt den Provider-Namen als Argu- essenziellen Bestandteil des Pact-Workflows darstellen. Wir haben 64 iii iii iii www.ijug.eu iii
@Provider("account-service") @PactBroker class ProviderVerificationTest { private val authenticationProvider = mockk() @TestTemplate @ExtendWith(PactVerificationInvocationContextProvider::class) fun pactVerificationTest(pactContext: PactVerificationContext) { val service = AccountService(authenticationProvider) try { pactContext.verifyInteraction() } finally { clearAllMocks() service.shutdown() } } @State("jane.doe has password 123456") fun `jane doe has password 123456`() { every { authenticationProvider.authenticate("jane.doe", "123456") } returns true } } Listing 2: Provider-Pact-Test in Kotlin unter Verwendung von pact-jvm und JUnit 5 zwei Ansätze kennengelernt, wie so ein Austausch umgesetzt wer- Startet man einen Providertest, so genügt es, die Consumer-Version den kann: 1) Duplizieren der Verträge in die Provider-Repositories anzugeben, die man verifizieren will. Der Broker liefert dann die letz- oder 2) Herunterladen der Verträge beim Ausführen der Provider- te Version des Vertrags für diese Consumer-Version aus. tests. Entscheidet man sich für die zweite Option, so bietet sich der Einsatz des Pact Broker an. Austausch von Verification-Ergebnissen Sobald eine Verification abgeschlossen ist, kann der Provider die Der Broker agiert als Vermittler zwischen Consumer und Provider. Ergebnisse an den Broker senden. Jede Verification wird dabei mit Consumer veröffentlichen ihre Verträge beim Broker und Provider einer Providerversion assoziiert. Der Broker kombiniert nun die können sie herunterladen, um Verification-Tests auszuführen. Die Consumer-Version des Vertrags mit der Providerversion der Verifi- Ergebnisse der Verification-Tests können ebenfalls über den Broker cation, um die Verification-Matrix zu aktualisieren. bereitgestellt werden. Anhand dieser kann festgestellt werden, wel- che Komponentenversionen miteinander kompatibel sind. Die Verification-Matrix (siehe Abbildung 1) speichert die Verification-Er- gebnisse sowie den Zeitpunkt der Veröffentlichung gemeinsam mit den Was leistet der Pact Broker? jeweiligen Consumer-, Provider- und Contract-Versionen sowie den Tags. Der Pact Broker bietet mehrere Funktionen an, die an unterschiedli- chen Stellen des Entwicklungsprozesses eingesetzt werden können: Mithilfe dieser Informationen kann man die Kompatibilität zwi- Austausch von Pact-Files, Austausch von Verification-Ergebnissen, schen einer spezifischen Consumer- und einer Provider-Version Tagging und Webhooks. Im Folgenden wollen wir diese Funktionali- überprüfen, ohne erneut eine Verification durchführen zu müssen. täten im Detail beleuchten. Der „Can-I-Deploy“-Befehl [3] implementiert diese Kompatibilitäts- überprüfung als Teil des Pact-Broker-Kommandozeilenwerkzeugs. Austausch von Pact-Files Nach Eingabe der Pacticipant-Versionen (Consumer und Provider Wenn ein Consumer einen neuen Vertrag generiert, benötigt er üb- werden in Pact als „Pacticipants“ bezeichnet) wird ausgegeben, ob licherweise eine Verification von jedem seiner Provider. Consumer diese Versionen gemeinsam bereitgestellt werden können. können neue Verträge an den Broker schicken, damit die Provider diese herunterladen können. Aber woher weiß ein Provider, welche Will man jedoch überprüfen, ob ein Dienst in die Produktionsum- Verträge er verifizieren soll? gebung bereitgestellt werden kann, so müsste man genau wissen, welche Versionen aller anderen Dienste gerade in Produktion aus- Will ein Consumer einen Vertrag hochladen, so muss er eine Consu- gerollt sind. Wie können wir also überprüfen, ob wir unseren Fea- mer-Version angeben. Diese Version sollte die Softwareversion des ture-Branch ohne Bedenken mergen können? Die Antwort liegt in Consumers repräsentieren. Hierbei muss kein spezielles Schema der Verwendung von Tags. befolgt werden, wie beispielsweise Semantic Versioning. Es ist im Allgemeinen üblich, einfach den Commit-Hash zu verwenden, unter Tagging dem der Vertrag generiert wurde. So lässt sich anhand eines Ver- Der Broker ermöglicht es, einer Pacticipant-Version einen Tag („Eti- trages auch leicht zuordnen, in welchem Stand sich der Code des kett“) zuzuweisen. Ein Tag ist eine Zeichenkette, die eine bestimmte Consumers bei der Generierung befand. Version markiert. Java aktuell 01/21 65
Abbildung 1: Verification-Matrix auf der UI eines Pact Broker Für Tags gibt es zwei Haupteinsatzzwecke: • Veröffentlichung eines neuen Pact-Files • Veröffentlichung eines neuen Pact-Files mit geändertem Inhalt, 1. Anzeigen, dass eine Version in einer bestimmten Umgebung be- aktualisierten oder neuen Tags reitgestellt ist, indem die Version mit dem Umgebungsnamen • Veröffentlichung von Verification-Ergebnissen getaggt wird, zum Beispiel „test“ oder „prod“. 2. Zuweisen einer Version zu einem Feature-Branch, indem der Zusätzlich kann spezifiziert werden, ob nur ausgewählte Pactici- Branch-Name getaggt wird, etwa „ABC-123/new-login“. pants in Betracht gezogen werden sollen. Immer wenn ein entspre- chendes Ereignis eintritt, schickt der Broker einen HTTP-Request an Das Taggen der Umgebung ist nützlich, wenn can-i-deploy ein- die konfigurierte URL. gesetzt wird, da man so nicht die exakten Versionen aller anderen Pacticipants kennen muss. Stattdessen fragt man: Ist es sicher, die- Des Weiteren sind auch der HTTP-Header und -Body sowie Basic- se Version in dieser Umgebung bereitzustellen? Authentication-Zugangsdaten konfigurierbar. Dabei kann auf dy- namische Variablen [4] zurückgegriffen werden, um somit weitere Das Taggen des Feature-Branch hingegen hilft dabei, neue Verträge Anpassungen am Request vorzunehmen: in allen Pacticipants zu verifizieren, bevor man den jeweiligen Code in den Masterbranch integriert. Das Taggen von Consumer-Versionen • Consumer- und Provider-Name mit den Branch-Namen ermöglicht es den Providern, die letzte Versi- • Consumer- und Provider-Version on der Pact-Files von diesem Feature zu verifizieren. Wählt man den • Consumer- und Provider-Tags gleichen Branch-Namen im Consumer- und im Provider-Repository, • Consumer- und Provider-Labels dann lässt sich die Konfiguration der Providertests einfach umsetzen, • URL des Pact-Files sodass der entsprechende Vertrag vom Consumer angezogen wird. • URL des Verification-Ergebnisses Webhooks Dank Webhooks lassen sich problemlos eigene Integrationen mit Das Einsatzgebiet von Webhooks liegt in der Benachrichtigung an- den vorhandenen Build-Pipelines und anderen Tools wie Jira oder derer Komponenten innerhalb des Entwicklungsprozesses im Falle Slack bauen. Die nächsten Abschnitte demonstrieren am Beispiel einer Änderung im Pact Broker. Sie können für die folgenden Ereig- von GitLab-Pipelines, wie man die kennengelernten Konzepte in der nisse konfiguriert werden: Praxis umsetzen kann. 66 iii iii iii www.ijug.eu iii
Abbildung 2: Relevante Komponenten für den Aufbau einer Entwicklungspipeline mithilfe des Pact Broker. Die CI-Pipelines sind mit GitLab CI implementiert und der Pact Broker dient als Vermittler Aufbauen einer Build-Pipeline sein muss, bevor die nächste gestartet wird. Build-Artefakte wer- Im Folgenden wird ein Beispielszenario beschrieben. Basierend auf den zwischen den Stages übergeben. Die für unser Beispiel relevan- dem Fallbeispiel aus den ersten Abschnitten des Artikels, wollen ten Stages befinden sich in Listing 3. wir uns im Folgenden die GitLab-Repositories und -Pipelines des in JavaScript geschriebenen Consumers und des auf der JVM laufen- Consumer-Pipeline: den Providers ansehen (siehe Abbildung 2). Erzeugen und Veröffentlichen von Verträgen In der Pipeline der Login-View generieren wir die Verträge wäh- Der Consumer veröffentlicht die Pact-Files über den Pact Broker rend der build-Stage. Wir können sie dann entweder direkt an als Teil eines Build-Jobs. Ein Webhook, basierend auf neu veröffent- den Pact Broker übermitteln oder als Build-Artefakte an nachfol- lichten Verträgen, startet eine Provider-Verification in Form einer gende Stages übergeben. Listing 4 illustriert den Pipelinejob, der dedizierten Build-Pipeline. Die Ergebnisse der Verification werden die Verträge im pacts-Ordner erzeugt und diese dann als Build- wiederum an den Pact Broker übermittelt. Artefakt bereitstellt. Durch die Anlage von Versionstags im Anschluss an ein erfolgrei- Der darauffolgende Job verwendet das pact-broker-Programm aus ches Deployment ermöglichen wir den Einsatz von can-i-deploy. dem pactfoundation/pact-cli-Docker-Image, um die Verträge an Somit kann innerhalb der Pipelines geprüft werden, ob die aktuelle den Broker zu übermitteln. Listing 5 beinhaltet den dafür verwende- Version ohne Fehler ausgerollt werden kann. ten YAML-Ausschnitt. Die Codebeispiele in den nachfolgenden Abschnitten beziehen sich Beim Veröffentlichen verwenden wir den Commit-Hash von Git als auf GitLab CI Build-Pipelines, die in .gitlab-ci.yml-Dateien defi- Consumer-Version und taggen den Vertrag mit dem aktuellen Bran- niert sind. Jeder Pipeline-Lauf besteht aus mehreren Stages („Pha- ch-Namen. Die Argumente broker-base-url und broker-token sen“), wobei die vorhergehende Stage zunächst erfolgreich beendet pact-test: image: node:latest stages: stage: build - build script: - publish # (consumer only) - "npm run test:pact" - can-i-deploy artifacts: - deploy paths: - tag - pacts Listing 3: GitLab CI Pipeline-Stages, auf die wir uns in den Listing 4: GitLab CI Pipeline-Job zum Ausführen der Consumer-Tests folgenden Listings beziehen und zum Erzeugen der Pact-Files. pact-publish: image: pactfoundation/pact-cli:latest stage: publish script: - "pact-broker publish pacts --consumer-app-version=$CI_COMMIT_SHORT_SHA --tag=$CI_COMMIT_REF_NAME --broker-base-url=$PACT_BROKER_BASE_URL --broker-token=$PACT_BROKER_API_TOKEN" Listing 5: GitLab CI Pipeline-Job zum Veröffentlichen der Pact-Files an den Broker Java aktuell 01/21 67
{ "description": "Trigger user-service verification", "provider": { "name": "user-service" }, "enabled": true, "request": { "method": "POST", "url": “https://gitlab.com/api/v4/projects/1122344/ref/master/trigger/pipeline?token=12345678&variables[PA CT_CONSUMER_TAG]=${pactbroker.consumerVersionTags}”, "body": "" }, "events": [{ "name": "contract_content_changed" }] } Listing 6: Pact-Broker-Webhook-Ressource im JSON-Format werden über GitLab-Umgebungsvariablen eingefügt. Diese kann pact-verify: man in GitLab auf Gruppenebene konfigurieren, sodass sie jedem stage: build Projekt in der Gruppe zur Verfügung stehen. script: - ./gradlew pactTest only: Nachdem wir nun die Consumer-Pipeline aufgesetzt haben, können - triggers wir dazu übergehen, die Provider-Verification durch einen Pipeline- trigger aufzurufen. Listing 7: GitLab CI Pipeline-Job zum Ausführen der Provider-Verifica- tion-Tests Providerpipeline: Ausführen von Verification-Tests Wie wir bereits wissen, kann der Pact Broker durch Webhooks auf die Veröffentlichung von Verträgen reagieren. Damit wir eine pact-tag: GitLab-Pipeline programmatisch starten können, muss zunächst image: pactfoundation/pact-cli:latest ein Pipeline-Token [5] erzeugt werden. Anschließend verwenden wir stage: tag script: den dabei angelegten Pipeline-Trigger, um einen Webhook auf dem - "pact-broker create-version-tag Pact Broker anzulegen. Listing 6 zeigt den JSON-Payload einer sol- --pacticipant=user-service chen Webhook-Ressource. --version=$CI_COMMIT_SHORT_SHA --tag=production --broker-base-url=$PACT_BROKER_HOST Beim Aufrufen der Pipeline übergeben wir die PACT_CONSUMER_ --broker-token=$PACT_BROKER_API_TOKEN" only: TAG-Variable, damit wir im Provider die korrekten Pact-Files zur refs: Verification vom Broker abfragen können. Der Broker ersetzt die - master ${pactbroker.consumerVersionTags}-Variable automatisch. Listing 8: Gitlab CI Pipeline-Job zum Anlegen eines Version-Tags für Da wir jedoch bei einer solchen Pipeline nur die Verification-Tests die Produktionsumgebung ausführen wollen und keine anderen Jobs, können wir von der except/only-Funktionalität [6] Gebrauch machen. Listing 7 illust- riert die entsprechende Job-Definition. pact-can-i-deploy: image: pactfoundation/pact-cli:latest In diesem Fall wird der Job nur ausgeführt, wenn die Pipeline durch stage: can-i-deploy script: einen API-Aufruf gestartet wird. Damit im Umkehrschluss bei einem - "pact-broker can-i-deploy API-Aufruf durch den Pact Broker jedoch nicht alle anderen Jobs mit --pacticipant login-view ausgeführt werden, muss dort jeweils das except-Schlüsselwort --version $CI_COMMIT_SHORT_SHA --to production verwendet werden, um Trigger auszuschließen. --broker-base-url $PACT_BROKER_BASE_URL --broker-token $PACT_BROKER_API_TOKEN" Während des pact-verify-Jobs führen wir das selbstgeschriebe- ne Gradle-Task pactTest aus. Es zieht die korrekten Pact-Files vom Listing 9: GitLab CI Pipeline-Job zum Abfragen der Verification- Broker an, indem es die Umgebungsvariable System.env.PACT_ Matrix vor dem Bereitstellen einer neuen Version in der Produktions- CONSUMER_TAG auswertet, die vom Webhook übertragen wird. Wenn umgebung die Tests richtig konfiguriert sind, dann werden die Verification-Er- gebnisse anschließend wieder zurück an den Broker übermittelt. Can I Deploy? Jedes Verification-Ergebnis ist mit einer Kombination aus ent- Kann ich meine Änderungen bereitstellen? sprechender Consumer- und Provider-Version versehen, damit die Der can-i-deploy-Befehl fragt die Verification-Matrix ab. Um Verification-Matrix aktualisiert werden kann. Diese kann nun wäh- jedoch die wichtige Frage „Kann ich diese Änderung in Produktion rend des Deployment-Prozesses abgefragt werden, um heraus- bereitstellen?“ beantworten zu können, müssen wir zunächst dem zufinden, ob die gewünschte Version in Produktion bereitgestellt Pact Broker mitteilen, welche Pacticipant-Version in Produktion ak- werden kann. tuell bereitgestellt ist. 68 iii iii iii www.ijug.eu iii
Dies geschieht durch Aufruf von create-version-tag. Hierfür le- gen wir einen Pipeline-Job an, der direkt nach einem erfolgreichen Deployment ausgeführt wird. Listing 8 zeigt einen YAML-Schnipsel, der den pact-tag-Job definiert. Dieser legt einen Version-Tag ba- sierend auf der bereitgestellten Pacticipant-Version an. Sollten mehrere Umgebungen (wie zum Beispiel Test oder Staging) vorhanden sein, so können selbstverständlich auch für diese ent- sprechende Version-Tags gesetzt werden. Nachdem wir nun das Tagging zusammengesteckt haben, können wir einen letzten Schritt in die Pipeline einbauen, der vor dem ei- gentlichen Bereitstellen can-i-deploy aufruft und die Pipeline abbricht, sollten keine erfolgreiche Verifications vorhanden sein. Listing 9 beinhaltet den YAML-Ausschnitt des can-i-deploy-Jobs in der Login-View. Es gibt mehrere Arten, den can-i-deploy-Befehl aufzurufen. In unserem Anwendungsfall geben wir den Pacticipant, seine Version sowie die Umgebung, in die ausgerollt werden soll, an. Mit anderen Worten: Wir überprüfen, dass keine fehlenden oder fehlgeschlage- nen Verification-Ergebnisse zwischen der Login-View, die wir ak- spielsweise eine Slack-Anwendung entwickelt, die Entwickelnde tuell bereitstellen wollen, und ihren Consumern und Providern, die über Vertragsänderungen und abgeschlossene Verifications in- bereits in Produktion bereitgestellt sind, existieren. formiert sowie ihnen per Slash-Command erlaubt, Provider-Veri- fications anzustoßen. Wenn der Job fehlschlägt und die Pipeline korrekt konfiguriert ist, dann wird die Pipeline gestoppt und die Bereitstellung abgebrochen. Eine weitere sinnvolle Funktionalität wäre eine Integration mit Mer- Es gilt zu beachten, dass ein fehlgeschlagener Job nicht zwangsläu- ge- oder Pull-Requests, sodass diese ohne erfolgreiche Verifica- fig durch inkompatible Versionen hervorgerufen wird. Es kann auch tion-Ergebnisse nicht gemergt werden können. bedeuten, dass die Verification-Ergebnisse noch nicht eingetroffen sind. Basierend darauf, wie die Pipelines konfiguriert sind, kann es Man sollte ebenfalls bedenken, dass es sich bei Pact um eine sehr also zu einer Race-Condition zwischen den Provider-Verification- junge Technologie handelt. Die Dokumentation ist über verschie- Tests und der Consumer-Pipeline kommen. dene Projekte und Websites verteilt. Es kostete uns viele Wochen, bis wir eine funktionierende Pact-Broker-Integration hatten. Wir Herausforderungen beim Einsatz des Pact Broker können dabei sehr die SaaS-Lösung von pactflow.io empfehlen, die Auf der einen Seite ist der Pact Broker ein sehr nützliches Werk- zusätzlich zum Pact Broker noch eine einfach zu verwendende Be- zeug zur Implementierung eines Consumer-Driven Contract-Tes- nutzeroberfläche bietet. ting-Workflows. Auf der anderen Seite gibt es jedoch einige Stol- perfallen und Herausforderungen, auf die wir im Folgenden gerne Diskussion eingehen wollen. Wir haben den Pact-Workflow kennengelernt und gesehen, wie man ihn mithilfe des Pact Broker und GitLab CI umsetzen kann. Al- Wenn man Pact einsetzt, wird so manche Problematik der verteilten lerdings ist Pact keine Wunderwaffe, sondern hat einen spezifischen Softwareentwicklung auf einmal sichtbar. Das ist an sich erst mal Einsatzzweck. kein Nachteil, jedoch kann es von Zeit zu Zeit verwirrend sein, wenn plötzlich Pipelines fehlschlagen und man sich auf die Suche nach der Pact funktioniert hervorragend, wenn man Interaktionen zwischen fehlenden Verification begeben muss. verteilten Diensten testen will, ohne dabei die Komplexität von End- to-End-Tests auf sich zu nehmen. Allerdings ist Pact auch mit Auf- Zusätzlich ist es alles andere als trivial, einen Entwicklungsworkflow wand verbunden. Sollte man also mit einem Monolithen auskom- umzusetzen, der Versionsverwaltung, die Build-Infrastruktur und men und so ein verteiltes System vermeiden können, dann sollte den Pact Broker integriert und dabei noch den individuellen Ansprü- das die präferierte Lösung sein. Pact lässt sich in der Testpyramide chen der Teams genügt. Wie so oft steckt der Teufel im Detail. Die irgendwo zwischen End-to-End-Tests und Unittests ansiedeln. Umsetzung hängt davon ab, ob man langlebige Feature-Branches oder Trunk-Based-Development verwendet, ob man GitLab CI oder Wenn ein Unternehmen jedoch auf unabhängig voneinander ent- Jenkins benutzt, ob man eine oder mehrere Laufzeitumgebungen wickelte und bereitgestellte Dienste setzt, um seinen Entwick- besitzt und so weiter. lungsprozess über mehrere Teams hinweg zu skalieren, kann Pact helfen, die Diskussionen zwischen den Teams zu unterstützen. Es Während Webhooks einem sehr viel Flexibilität geben, andere erleichtert ebenfalls API-First-Design, erhöht das Vertrauen in den Tools in den Pact-Workflow einzubinden, gibt es aktuell leider Bereitstellungsprozess und erlaubt, Schnittstellen ohne explizite noch sehr wenige vorgefertigte Integrationen. Wir haben bei- Schnittstellen-Versionierung über die Zeit zu verändern. Java aktuell 01/21 69
Verträge können als Schnittstellendokumentation verwendet wer- Referenzen den. Ähnlich wie Unittests das Verhalten des getesteten Moduls [1] Pact Foundation (2020): Pact Specification dokumentieren, sind Pact Files hilfreich, um zu verstehen, wie mit https://github.com/pact-foundation/pact-specification/ einem Dienst interagiert werden kann. [2] https://docs.pact.io/getting_started/matching/ [3] Pact Foundation (2020): Can I Deploy https://docs.pact.io/ Außerdem sollte man nicht vergessen, dass konsumentengetrieben pact_broker/can_i_deploy nicht konsumentendiktiert bedeutet. Consumer-Teams sollten nicht [4] Pact Foundation (2020): Webhooks, Dynamic Variable einfach neue Anforderungen an die Provider-Teams schicken und Substitution https://github.com/pact-foundation/pact_ erwarten, dass diese eins zu eins umgesetzt werden. Stattdessen broker/blob/master/lib/pact_broker/doc/views/webhooks. sollten Consumer-Teams die Diskussion initiieren und die Entwick- markdown#dynamic-variable-substitution lung und Evolution der Schnittstelle treiben. Provider sollten bei Än- [5] GitLab Inc. (2020): Triggering pipelines through the API derungen keine bestehenden Verträge verletzen. Pact ist kein Werk- https://docs.gitlab.com/ee/ci/triggers/ zeug, das Kommunikation ersetzt. [6] GitLab Inc. (2020): GitLab CI/CD Pipeline Configuration Reference https://docs.gitlab.com/ee/ci/yaml/#onlyexcept-basic Hinzu kommt, dass Pact sich nicht für öffentliche Schnittstellen eig- net, bei der die Consumer nicht bekannt sind. In diesem Fall ist es günstiger, auf eine Kombination aus OpenAPI und einem Tool wie Hikaku zu setzen. Unabhängig davon, ob man sich dafür entscheidet, Pact-Files ma- nuell zu kopieren oder den Pact Broker einzusetzen, ist es von gro- ßer Wichtigkeit, dass alle Entwickelnden mit den Konzepten von Pact vertraut sind. Andernfalls riskiert man Frustration und Fehler. Zusammenfassung Wir haben gesehen, wie man Pact verwenden kann, um Interaktio- nen zwischen verteilten Diensten zu testen. Consumer-Tests erzeu- gen Verträge in Form von Pact-Files, die wiederum von Providern verifiziert werden müssen. Frank Rosner Die Pact-Spezifikation ist in vielen verschiedenen Plattformen und codecentric AG Programmiersprachen implementiert, sodass Pact problemlos frank.rosner@codecentric.de sprachübergreifend eingesetzt werden kann. Der Austausch von Pact-Files kann auf unterschiedliche Art umgesetzt werden. So ist Franks Interessen liegen im Bereich von Big Data, Machine sowohl das manuelle Committen in den Provider-Repositories als Learning, Cloud-Native-Anwendungen und Softwareentwick- lung. Er liest gerne Paper and Quelltexte, um zu verstehen, wie auch die Verwendung eines Pact Broker eine mögliche Strategie. die Dinge funktionieren, die er verwendet. Auf der JVM entwi- ckelt er in Scala, Kotlin und Java. Durch den Einsatz von Pact wird die evolutionäre Anpassung von Schnittstellen unterstützt, sofern alle Consumer Teil des Pact- Workflows sind. Auf der anderen Seite ist Pact nicht für öffentliche Schnittstellen geeignet, bei denen nicht alle Consumer bekannt sind. Wenn die Entwickelnden erste Erfahrungen mit Pact gesammelt ha- ben, ist der Pact Broker ein nützliches Werkzeug, um den Workflow weiter automatisieren. Er ermöglicht, Pact-Files und Verification- Ergebnisse auszutauschen sowie die Integration in andere Systeme wie Slack, Jira, GitLab etc. mithilfe von Webhooks voranzutreiben. Bei der Integration des Brokers in die eigenen GitLab-CI-Pipelines können mithilfe von eigenen Job-Definitionen Pact-Files veröffent- licht, Provider-Verification-Tests angestoßen, Bereitstellungen ge- Raffael Stein taggt sowie Verification-Ergebnisse abgefragt werden. codecentric AG raffael.stein@codecentric.de Jedoch kann gerade bei einer neuen Technologie wie Pact das Ein- richten und Zusammenstecken anspruchsvoll und zeitaufwendig Raffael ist ein Software-Ingenieur mit Fokus auf Backend- sein. Man muss viel Dokumentation lesen und teils sogar Quell- Technologien. Seine Leidenschaft liegt bei Microservices- Architekturen und dem Entwickeln wartbarer Software in texte, da die Dokumentation unzureichend ist. Aber gerade bei autonomen Teams. Die JVM ist schon seit vielen Jahren sein Microservices-Architekturen, bei denen Schnittstellen teamüber- technisches Zuhause. greifend entworfen und angepasst werden müssen, spielt Pact seine Vorteile aus. 70 iii iii iii www.ijug.eu iii
Sie können auch lesen