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

2 Kommentare

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.
Services

  • 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.

Docker

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.

Build

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.

Deployment

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?

Tobias Flohre

Tobias Flohre arbeitet als Senior-Softwareentwickler/Architekt bei der codecentric AG. Seine Schwerpunkte sind Java-Enterprise-Anwendungen und Architekturen mit JEE/Spring. Er ist Autor diverser Artikel und schreibt regelmäßig Blogbeiträge zu den Themen Architektur und Spring. Zurzeit beschäftigt er sich mit Integrations- und Batch-Themen im Großunternehmen sowie mit modernen Webarchitekturen.

Share on FacebookGoogle+Share on LinkedInTweet about this on TwitterShare on RedditDigg thisShare on StumbleUpon

Kommentare

  • Hi Tobias! Sehr schöner Artikel! Eine Frage habe ich dazu:

    „das heißt, dass der Docker-Container mit dem Service in dem Modus isoliert gestartet werden kann, keine weiteren Abhängigkeiten benötigt“

    Dies ist ja schon eine sehr weitreichende Forderung. Was ist wenn mein Contract Test ein bestimmtes Antwortverhalten verlangt und der zu testende Dienst dieses Verhalten nur liefert, wenn abhängige Services auch wirklich vorhanden sind?

    Woher weiß ich das mein Kontrakt nicht gebrochen wird, wenn der getestete Dienst „echte“ Abhgängigkeiten verwendet?

    Vielleicht sollte man die Forderung nach „keinen Abhängigkeiten“ etwas aufweichen…

    • Tobias Flohre

      25. Oktober 2016 von Tobias Flohre

      Ja, das ist eine weitreichende Forderung. Allerdings würde ich nicht den Weg gehen, weitere Abhängigkeiten notwendig zu machen, weil man dann nicht mehr leicht automatisiert die notwendigen Anwendungen hochfahren kann – woher will man wissen, welche weiteren Abhängigkeiten in welchen Versionen man benötigt? Außerdem ist man dann schon ziemlich nah an Integrationstests in vollständigen Testumgebungen.
      Wenn ich ein bestimmtes Antwortverhalten verlange, das die Anwendung bisher im Self-Contained-Modus nicht bereitstellt, dann schreibt man halt den Test und verlangt von der anderen Anwendung, dass dieses Antwortverhalten bereitgestellt wird. Wenn sie das Antwortverhalten mit echten Abhängigkeiten zeigt, dann kann sie es auch im Self-Contained-Modus zeigen. Und wenn sie es gar nicht zeigen kann, sollte man mal reden.

Kommentieren

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert.