Beliebte Suchanfragen
//

Contract Testing: Testen in einem Deploy-To-Production-Whenever-You-Want-Szenario

5.9.2016 | 5 Minuten Lesezeit

Continuous Deployment ist bisher nur in sehr wenigen Unternehmen angekommen. Die Regel sind immer noch eine Handvoll Releases im Jahr, die in einer mehrwöchigen manuellen Testphase getestet werden. Releases von verschiedenen Anwendungen werden gebündelt und gleichzeitig auf die Test- und Produktionssysteme gebracht. Es gibt Code-Freeze-Phasen, in denen nicht mehr entwickelt werden darf, und jede Menge Hotfixes, um Fehler doch noch zu beheben. Oder um gleich ein neues Feature hinterherzuschieben, das vor dem Code-Freeze nicht mehr fertig wurde.

Wie kommt man von solch einem System zu einem System, in dem jeder in Produktion gehen kann, wann er möchte, ohne dass das Gesamtsystem instabiler wird?

Ziel ist es, durch automatisierte Tests möglichst frühzeitig Probleme zu erkennen. Und das ist schwer! Unit-Tests sind inzwischen gut verstanden, und man findet kaum noch jemanden, der sagen würde, dass Unit-Tests nicht sinnvoll sind. Integrationstests in derselben Anwendung sind ebenfalls gut verstanden, und beide können einfach im Build automatisiert durchgeführt werden. Problematisch wird es, wenn es um Tests geht, die mehrere Anwendungen bzw. Services integrieren. Selten findet man Oberflächen-Akzeptanztests, die sich in Testsystemen automatisiert durch die Anwendung klicken. Schwierig ist hierbei, dass die Tests stark davon abhängen, dass die richtigen Testdaten im Testsystem vorhanden sind, und dass die Tests deswegen häufig instabil sind. Auch nicht klar ist, wann genau diese Akzeptanztests durchgeführt werden sollen, da sie sich nicht auf eine Anwendung, sondern auf das Gesamtsystem beziehen. Und sie ermitteln Fehler nicht vor dem Deployment in die Testumgebung, sondern erst danach.

Wenn man zurzeit auf das Thema Microservices und Testing kommt, ist eigentlich immer von Consumer Driven Contracts die Rede. Grob gesagt geht es darum, dass der Konsument eines Services zusammen mit dem Provider des Service einen Kontrakt definiert und in einem Test prüft, ob der Kontrakt eingehalten wird. Wenn der Provider nun eine Version baut, kann durch Ausführen der Tests aller Consumer sichergestellt werden, dass der Provider immer noch alle Konsumenten bedienen kann.

Was brauchen wir, um Stabilität sicherzustellen?

Es gibt zwei Seiten, die betrachtet werden müssen. Sagen wir, wir haben einen Service A, der Service B aufruft. Service A schreibt einen Contract Test für Service B.

  • Beim Build von Service B wollen wir sicherstellen, dass der neue Build alle definierten Contracts einhält – da jeweils die aktuellste Version des Contracts.
  • Beim Deployment von Service A in eine Umgebung X wollen wir sicherstellen, dass der Contract, der von Service A definiert wurde, von der aktuell in der Umgebung deployten Version von Service B eingehalten wird.

Der zweite Punkt ist wichtig, weil er Überholer verhindert – wenn sich zwei Anwendungen auf einen Contract geeinigt haben, ist ja noch nicht klar, wann die Version deployt wird, die diesen Contract erfüllt. Durch den zweiten Punkt verhindern wir automatisiert Instabilität durch fehlerhafte Absprachen.

Und wie implementieren wir nun genau diese Contract Tests?

Es gibt Bibliotheken wie PACT oder Spring Cloud Contract , auf die ich in einem weiteren Blog Post eingehen werde. Vorher würde ich gerne ein Vorgehen vorstellen, dass noch nicht konkret von diesen Bibliotheken abhängt. Das Vorgehen hat die folgenden Voraussetzungen:

  • Es wird Docker verwendet, und Service A und Service B werden jeweils in einem Docker Container namens service-a bzw service-b betrieben.
  • Jeder Service kann in einem Self-Contained-Modus betrieben werden – das heißt, dass der Docker-Container mit dem Service in dem Modus isoliert gestartet werden kann, keine weiteren Abhängigkeiten benötigt und bei der gleichen Serie von Requests die gleichen Antworten liefert.

Dann werden die Tests folgendermaßen implementiert:

  • Service A stellt in seinem Git-Repository ein Unterverzeichnis/Projekt contract-service-a-service-b mit den Tests bereit. Dort liegt auch ein Docker-File, das mit einem docker run die Tests durchführt. Damit man es sich besser vorstellen kann, skizziere ich hier eine mögliche Implementierung:
    • Die Tests werden mit JUnit implementiert. Sie nutzen die Client-Zugriffsklassen des Service-A-Hauptprojekts und gehen gegen Service B, indem der Docker-DNS-Name service-b verwendet wird.
    • Es wird Maven verwendet. Der Docker-Container contract-service-a-service-b hängt von Java und Maven ab und kopiert bei Docker build den kompletten Projektinhalt in den Container.
    • Bei docker run werden nun alle Tests durch ein mvn test durchgeführt.
  • In der CD-Pipeline wird dafür gesorgt, dass beim Build von Service A auch der Docker-Container contract-service-a-service-b mit der gleichen Version gebaut wird.

Die Tests werden nun in der CI/CD – Pipeline beim Build von Service B genutzt:

  • Nach dem Build von Service B wird der Docker-Container service-b im Self-Contained-Modus gestartet.
  • In der Docker Registry werden nach Namenskonvention alle Images contract-*-service-b mit Tag LATEST gesucht und jeweils mit docker run gestartet. Die Tests werden durchgeführt. Falls ein Container Fehler bei der Testausführung meldet, wird der Build abgebrochen und das gebaute Image von Service B nicht in die Docker Registry gepusht.

Außerdem werden sie beim Deployment von Service A in Umgebung X genutzt:

  • Es wird ermittelt, in welcher Version Service B in Umgebung X läuft. Der Docker-Container service-b wird in dieser Version im Self-Contained-Modus für den Test gestartet.
  • Der Test-Container contract-service-a-service-b mit der Version des Service A, die wir deployen wollen, wird gestartet und somit getestet, ob die zu deployende Version von Service A mit der deployten Version von Service B klarkommt.
  • Das wird wiederholt für weitere Contracts, die Service A mit anderen Services hat.

Der größte Aufwand ist sicherlich die Bereitstellung eines Self-Contained-Modus für jeden Service bzw. jede Anwendung. Erfahrungen in bisherigen Projekten zeigen jedoch, dass so ein Modus viel Mehrwert bringt. Umgesetzt haben wir ihn, indem wir Service-Access-Klassen, die dafür verantwortlich waren, von Service A aus Service B aufzurufen, über Properties gesteuert durch Stub-Klassen ersetzt haben, die immer bestimmte Ergebnisse liefern. In der regulären Entwicklung war das sehr hilfreich, da die externen Services in den entsprechenden Umgebungen nicht immer alle Datenkonstellationen lieferten, die man für die Implementierung benötigte. Und für CI/CD hat man so eine sehr stabile Datenbasis für Oberflächen- und Contract-Tests.

Die Contract Tests selbst bieten schon einen großen Mehrwert, wenn sie sehr simpel gehalten werden. Allein schon eine Überprüfung, ob der Client mit der neuen API strukturell zurechtkommt, ist sehr hilfreich. Dank der stabilen Datenbasis von Service B im Self-Contained-Modus können aber beliebig aufwändige Tests geschrieben werden.

Was gewinnt man bei diesem Vorgehen?

Anwendungen können mit großer Sicherheit jederzeit nach Produktion gehen. Man schafft Unabhängigkeit im Deployment-Prozess. Manuelle Tests können zurückgefahren werden.

Was kostet dieses Vorgehen?

Der Self-Contained-Modus muss bereitgestellt werden. Und Consumer müssen Contract Tests schreiben.

Die sehr interessante Frage ist: Wiegt der Gewinn die Kosten auf?

Beitrag teilen

//

Gemeinsam bessere Projekte umsetzen.

Wir helfen deinem Unternehmen.

Du stehst vor einer großen IT-Herausforderung? Wir sorgen für eine maßgeschneiderte Unterstützung. Informiere dich jetzt.

Hilf uns, noch besser zu werden.

Wir sind immer auf der Suche nach neuen Talenten. Auch für dich ist die passende Stelle dabei.