Consumer Driven Contract Testing
Wenn Software Verträge schließt
Missverständnisse zwischen Menschen sind oft kaum bemerkbar, schwer auszuräumen und ziehen, je länger sie unaufgeklärt bleiben, immer gravierendere Konsequenzen nach sich. Genauso verhält es sich mit Kommunikationsproblemen zwischen Softwarekomponenten, zum Beispiel in Microservice-Architekturen, die – ganz im Sinne der Metapher – obendrein oft noch mit verschiedenen (Programmier-)Sprachen arbeiten. Um Missverständnisse zu reduzieren, haben wir Menschen uns angewöhnt, in wichtigen Angelegenheiten (wie Arbeitsverhältnissen, Versicherungen oder Ehen) schriftlich festzuhalten, was die Beteiligten voneinander erwarten. Wir schreiben die jeweiligen Verbindlichkeiten in einem Vertrag auf und versprechen mit unseren Unterschriften, dass wir uns an die Vertragsvorgaben halten werden. Contract Testing überträgt diese Idee in die Welt der Software-Tests, um Missverständnisse zwischen APIs und ihren Nutzern (Consumern) zu vermeiden.
Bisher wurde die korrekte Zusammenarbeit von API und Consumer meist bei den Integrationstests sichergestellt. Da diese allerdings eine lauffähige Systemlandschaft benötigen, können Fehler erst relativ spät erkannt werden. Bei der Entwicklung ist es jedoch von Vorteil, das Feedback so früh wie möglich zu erhalten. Hinzu kommt, dass Integrationstests eigentlich zum Prüfen der Fachlichkeit dienen. In heutigen Testlandschaften stellen sie streng genommen "nur nebenher" fest, wenn es technische Probleme an der Schnittstelle gibt. Das kann sowohl bedeuten, dass der Aufwand bei der Fehlersuche steigt, weil neben fachlichen Problemen auch technische Ursachen zum Fehlschlag des Tests führen können, als auch der Wartungsaufwand der Tests sich erhöht.
Schöner wäre es also, wenn es dezidierte Tests gäbe, um Fehler an Schnittstellen zu finden. Hier bietet sich Contract Testing, genauer gesagt, Consumer Driven Contract Testing (CDCT) an: Verträge für Softwarekomponenten. Der Zusatz "Consumer Driven" bedeutet, dass der Vertrag vom Consumer bestimmt wird (man nennt ihn deshalb auch Consumer Contract) und die API sich nach dessen Vorgaben richten muss.
Neben Consumer Contracts gibt es die etwas verbreiteteren Provider Contracts [1]. Klassische Beispiele sind WSDL-/XML-Schemata von SOAP-APIs oder Werkzeuge wie Swagger im Kontext von REST-Services [2].
Wie funktioniert CDCT (am Beispiel von Pact)
Auch wenn sich die Funktionsweise von Framework zu Framework unterscheidet, bleibt die Idee dahinter gleich: Der Consumer definiert einen Vertrag, der vom Provider erfüllt werden muss.
Der Vertrag enthält die API samt Parameter und die erwartete (minimale) Antwort. Dabei kann die Beschreibung der Parameter durch den Einsatz von regulären Ausdrücken beliebig präzise sein. So kann der Consumer erwarten, beim Aufruf der API einen unbestimmten String-Parameter oder einen bestimmten String-Parameter aus einem vordefinierten Set zu übergeben. Der zweite Fall ist äußerst hilfreich, wenn der Provider diesen String-Parameter in einen Enum-Wert übersetzt. Am Beispiel von Pact, einem Framework für CDCT, soll die Funktionsweise näher erläutert werden [3].
Consumer Test
Sämtliche Verträge zwischen dem Consumer und dem Provider werden in einer JSON-Datei, auch Pact-Datei genannt, definiert. Der Consumer generiert diese Pact-Datei beim Ausführen seiner Pact-Tests und veröffentlicht sie auf dem Pact-Broker. Die Tests sind gewöhnliche Unit-Tests und basieren auf einem der vielen von Pact unterstützen Test-Frameworks wie JUnit oder Jest. In dem Test ruft der Consumer die zu testende API inklusive Parameter auf. Der Aufruf wird von einem Mock-Provider, der vom Pact-Framework zur Verfügung gestellt wird, entgegengenommen. Der Mock-Provider prüft, ob dieser Aufruf mit dem Aufruf aus dem Vertrag übereinstimmt. Sind keine Abweichungen gefunden, antwortet der Mock-Provider mit der minimalen Antwort, die ebenfalls aus dem Vertrag stammt. Der Consumer empfängt in seinem Test die Antwort und vergleicht sie mit seiner Erwartung. Stimmen sie überein, ist der Test erfolgreich. Auf diesem Weg stellt der Pact-Test sicher, dass der Consumer den Aufruf tätigen und die Antwort verarbeiten kann.
Der Pact-Broker ist ein Webserver, der sowohl vom Consumer als auch vom Provider erreichbar sein muss. Er verwaltet die Verträge, indem er sie versioniert ablegt, sie dem Provider zur Verfügung stellt und die Testergebnisse speichert. Die Art und Weise wie der Consumer die Verträge auf dem Pact-Broker veröffentlicht, ist abhängig von der eingesetzten Programmiersprache der Tests und/oder dem eingesetzten Dependendency-Management. Für Maven beispielsweise gibt es ein spezielles Pact-Plugin, das den publish-Goal anbietet, für JavaScript hingegen eine spezielle Pact-Version pact-js [4].
Provider Test
Analog zum Consumer-Test ist der Provider-Test ebenfalls ein Unit-Test und wird im Rahmen der restlichen Unit-Tests ausgeführt. Hierbei werden alle Verträge, die der Provider mit seinen Consumern abgeschlossen hat, vom Pact-Broker heruntergeladen. Die APIs werden daraufhin lokal aufgerufen, sodass ein Aufruf durch den Consumer simuliert wird. Der Provider verarbeitet den Aufruf und generiert seine Antwort, die mit der erwarteten Antwort aus dem Vertrag verglichen wird. Sollten sie gleich sein, hat der Provider den Pact-Test bestanden. Ansonsten ist der Test fehlgeschlagen und das Artefakt des Providers kann nicht ordnungsgemäß gebaut werden. Die Ursache für Fehlschläge sind in den meisten Fällen Code-Änderungen am Provider ohne den Consumer entsprechend angepasst zu haben oder die Erwartungshaltung des Consumers ist fehlerhaft. In beiden Fällen wird durch CDCT ein Missstand aufgedeckt, der erst zur Laufzeit bemerkbar gewesen wäre.
Eine Übersicht von CDCT-Tools
Natürlich ist Pact nicht das einzige Framework zum Implementieren von CDCT, wenn auch aktuell das am weitesten verbreitete. Neben Pact existieren noch folgende Möglichkeiten, in das vertragsbasierte Testen einzusteigen:
- Postman bietet mit Collections ein Toolset, um Teams CDCT zu ermöglichen. Anders als Pact ist Postman jedoch kein spezialisiertes Framework für CDCT, sondern kann von Entwickler:innen aufgrund seiner breiten Landschaft an Werkzeugen unter anderem für CDCT eingesetzt werden [4].
- Wer sich ohnehin im Spring-Cloud-Umfeld bewegt, kann das Spring Cloud Contract Project nutzen, um Vertragstests in seiner Systemlandschaft einsetzen. Hier werden die Contracts als .groovy-Skripte erstellt. Bei Spring kommt kein Vertrags-Broker als Mittelsmann zum Einsatz. Stattdessen stellt der Provider den Vertrag bereit, gegen den der Consumer als Stub testen kann [5].
- Zusätzlich existiert das Tool Dredd, mit dem – wenn gewünscht – völlig sprachunabhängig eine API gegen Consumer Contracts getestet werden kann. Dredd unterstützt für die Beschreibung der Consumer-Erwartungen API Blueprint, OpenAPI 2 und (experimentell) 3. Dredd stellt, anders als Pact, keine Mock-Antworten für Client-Anfragen bereit [6].
Einordnung in bestehende Testlandschaften
Consumer Driven Contract Tests werden lokal im Rahmen der Unit-Tests ausgeführt. Dies ist auch ihre größte Stärke. Sie sind auf der Test-Pyramide unten angesiedelt, decken jedoch Fehler auf, die für gewöhnlich in den Integrationstests auffallen würden, da bei Integrationstests eine lauffähige Systemlandschaft vorhanden sein muss. Beim Einsatz von CDCTs kann darauf verzichtet werden, weshalb Unstimmigkeiten zwischen Consumer und Provider schon beim Bauen der Artefakte und nicht erst nach dem Deployment der Artefakte erkannt werden können. Unter Umständen kann das eine große Zeitersparnis von mehreren Stunden bedeuten. Entwickler:innen bleibt es durch das rasche Feedback erspart, sich nach Stunden oder Tagen erneut in komplexe Codestellen einarbeiten zu müssen. Stattdessen können sie wesentlich kohärenter an Problemlösungen arbeiten. Ein weiterer Vorteil: Sind die Schnittstellen bereits durch CDCT abgedeckt, können Integrationstests vermehrt die Prüfung der fachlichen Korrektheit fokussieren.
Wann lohnt sich der Einsatz von Consumer Driven Contract Tests?
Hat man eine monolithische Architektur oder eine geringe Anzahl an Services, so dürfte der Einsatz von CDCTs nicht besonders sinnvoll sein. Hier wird es wahrscheinlich genügen, eine Dokumentation (z. B. mittels Swagger) zu pflegen, die die Schnittstellen der Services beschreibt, sodass man bei Änderungen eines Providers die aktuelle Spezifikation der API nachschlagen und den Consumer an diese anpassen kann.
Wo sich der Einsatz von CDCTs besonders anbietet, ist bei Anwendungen, die auf einer Microservice-Architektur basieren. Da hier jede Kommunikation zwischen Services über REST-APIs stattfindet und die Zahl der Schnittstellen entsprechend groß ist, steigt die Komplexität und damit die Herausforderung, alle Interaktionen der verschiedenen Services im Blick zu halten. Bei der Entwicklung von solchen Systemen kann durch den Einsatz von CDCT die Softwarequalität gewährleistet und die Zeit zur Fehlersuche reduziert werden. Mit Pact kann zusätzlich auf dem Pact-Broker der Graph dargestellt werden, der die Abhängigkeiten zwischen den einzelnen Services abbildet.
CDCT ermöglicht zwar das frühzeitige Aufdecken von Unstimmigkeiten zwischen Consumer und Provider, ersetzt aber natürlich nicht die Kommunikation zwischen den Entwickler:innen der beiden Seiten. Dieser Austausch hat weiterhin eine hohe Bedeutung sowohl beim Entwurf als auch bei der Umsetzung der APIs.
Durch den Einsatz von CDCT können Kommunikationsprobleme letztlich sowohl zwischen Entwickler:innen als auch Schnittstellen mit wenig Aufwand aufgedeckt werden.
- R. Holshausen: The curious case for the Provider Driven Contract
- Swagger
- Pact
- Postman
- Spring Cloud Contract Project
- Dredd
Weiter Informationen: