Verteilte Softwarearchitekturen setzen sich immer mehr durch. Damit stellt sich die Frage, wie die Interaktion der verteilten Dienste getestet werden kann. Zudem hat sich auch die Art, wie diese Dienste entwickelt und getestet werden, stark geändert: Mit Continuous Integration/Continuous Delivery (CI/ CD) werden automatisierte Tests kontinuierlich ausgeführt. Deshalb befasst sich dieser Artikel mit der Frage, wie für Microservices Integrationstests in CI/CD-Deployment-Pipelines ausgeführt werden können.
Anforderungen von CI/CD-Deployment-Pipelines
Betrachten wir zunächst einmal kurz verschiedene Testtypen für unseren Microservice. In der in Abbildung 1 dargestellten CI/ CD-Pipeline fehlen die Testebenen. Wenn durch einen Integrationstest die Interaktion zweier Dienste getestet werden soll, können deren CI/CD-Deployment-Pipelines mit einem sogenannten „fan-in“ miteinander verknüpft werden (siehe Abbildung 2).
Abb. 1: Vereinfachte Darstellung einer CI/CD-Deployment-Pipeline
Abb. 2: CI/CD-Pipelines mit Integrationstestphase
Die Integration der Dienste kann dann auf einer System-Integration-Testumgebung getestet werden. Berücksichtigen muss man dabei aber, dass mit dieser Testumgebung die Deployment-Pipelines miteinander verknüpft und so voneinander abhängig werden. Die Folge davon ist, dass die Pipeline wegen einer anderen Pipeline blockiert sein kann, wenn ein Team eine neue Version eines Dienstes veröffentlichen will.
Insbesondere für Microservices ist das ein Problem, gehört doch zu deren Kerneigenschaften, dass sie getrennt voneinander veröffentlicht („released“) werden können. Diese Möglichkeit, unabhängig zu „releasen“, wird noch mehr eingeschränkt, wenn die Integrationstests von einem Integrations- oder End-2-End-Testteam durchgeführt werden und nicht von den Entwicklungsteams selbst. Die Folge kann dann ein „Water-Scrum-Fall“ sein: Die Entwicklung der Dienste erfolgt zwar agil, wird dann aber von einer Integrations- und E2E-Testphase gefolgt. Ist dies der Fall, entstehen ein Flaschenhals und eine Übergabe der Verantwortung für die Integration und den Produktionsbetrieb des Dienstes von den Entwicklerteams an Test-, Release-, und Operations-Teams.
Folgen von CI/CD-Deployment- Pipelines für Integrationstests
Gerade bei Microservices ist es daher ratsam, Tester in die Entwicklerteams zu integrieren. Wenn möglich sollten die Entwicklerteams dann auch für die Integrationstests verantwortlich sein. Wie können die Entwicklerteams aber diese Aufgabe übernehmen und wie kann die Deployment-Pipeline optimiert werden, um die Integrationsszenarien effektiv und effizient zu testen? Um den Integrationstest mehrerer Dienste möglichst effizient gestalten zu können, ist es notwendig, ihn mit anderen Tests zu flankieren, insbesondere wie in Abbildung 3 abgebildet mit API- und Contract-Tests.
Abb. 3: Ausgebaute CI/CD-Deployment-Pipeline
Ein API (Application Programming Interface bzw. Programmierschnittstelle) ist wie ein Vertrag, in dem festgelegt wird, wie man den Dienst programmatisch aufrufen kann. Da Dienste in verteilten Systemen in der Regel über APIs und Events miteinander kommunizieren, kann mit einem API-Test sichergestellt werden, dass die Zugriffsmöglichkeiten eines Dienstes wie spezifiziert funktionieren. In den API-Tests kann also beispielsweise überprüft werden, ob die HTTP-Statuscodes und die Datentypen mit der Spezifikation übereinstimmen.
Wenn das API mithilfe von Tools wie Swagger (OpenAPI) spezifiziert ist, kann die Testgenerierung sogar mit einem auf Grundlage dieser Spezifikation erstellten Client automatisiert werden. Wichtig ist dabei, dass API-Standards, wie eine semantische Versionierung und Rückwärtskompatibilität bei notwendigen Änderungen, eingehalten werden.
Consumer Driven Contract (CDC)-Tests haben sich insbesondere für Microservices durchgesetzt, um Integrationsszenarien zu testen. Bei diesem Test wird in einem Vertrag die API-Benutzung festgelegt. Da basierend auf dem Vertrag Tests erstellt werden können, kann der Contract-Test einer neuen Produzenten-Version in dessen Deployment-Pipeline erfolgen, bevor diese Version für die abhängigen Dienste (Konsumenten) veröffentlicht wird. So werden die Integrationsszenarien getestet, ohne dass eine Integration der Dienste erfolgt. Damit bleiben die Deployment-Pipelines voneinander unabhängig (siehe Abbildung 4).
Abb. 4: CDC-Tests
Für den Konsumenten bietet der Contract-Test zudem den Vorteil, dass basierend auf den API-Vertrag ein Mock des Produzenten-Dienstes erstellt werden kann. Dieser Mock kann dann in der Pipeline des Konsumenten dazu verwendet werden, die Integration zu testen. Die Contract-Tests sind also ein sehr effektives Mittel, um die Anzahl an Integrationstests zu verringern. Sie machen es aber auch erforderlich, dass die Tests immer den aktuellen Stand der API-Benutzung abbilden.
Gehören Integrationstests in CI/CD-Deployment-Pipelines?
Auch wenn API- und Contract-Tests ausgeführt werden, bleibt die Frage, wie die Integration mehrerer Dienste getestet werden kann, ohne Deployment-Pipelines miteinander koppeln zu müssen. Erleichtert wird dies, wenn die Dienste als Docker-Container zur Verfügung stehen. Ist dies der Fall, vereinfacht das, den Produzenten-Dienst innerhalb der Deployment-Pipeline des Konsumenten zu instanziieren. Vereinfacht wird dies weiterhin mit Bibliotheken wie „TestContainers“ für Java.
Bei einem System mit vielen verteilten und voneinander abhängigen Diensten stößt dieses Vorgehen aber auch an seine Grenzen. Helfen kann deshalb, auch der rechten Seite der Deployment-Pipeline Beachtung zu schenken, also wie durch Deployment und Release Microservices veröffentlicht werden.
Anstatt durch Tests alle möglichen Integrationen von eventuell Hunderten Microservices unter realistischen Benutzungsszenarien abzubilden, setzt es sich mehr und mehr durch, sich mit den Tests zunächst lediglich auf die grundlegenden Integrationsszenarien zu konzentrieren, um so Microservices schnell auf Produktion veröffentlichen zu können. Die Idee dabei ist, Integrationsprobleme mithilfe von Monitoring so schnell es geht zur Laufzeit entdecken zu können. Für diese Art des Monitorings bieten sich Tests an, die die grundlegenden Benutzungsszenarien mithilfe von synthetischen Transaktionen testen.
Üblicherweise erfolgt das Release auf Produktion zudem nicht in einem Schritt: Entkoppelt man das Deployment vom Release, ergibt sich die Möglichkeit, eine neue Version eines Dienstes auf der Produktionsumgebung zu installieren, die neue Funktionalität dabei aber zu diesem Zeitpunkt noch nicht freizugeben. Vor dem eigentlichen Release kann zum Beispiel ein „Dark Release“- und „Shadow Deployment“-Test erfolgen. Werden „Feature Toggles“ unterstützt, kann zudem gezielt zur Laufzeit ein neues Feature für eine kleine Benutzergruppe freigeschaltet werden, um so den „Blast Radius“ klein und kontrollierbar zu halten. Schließlich kann das Veröffentlichen inkrementell erfolgen, also zum Beispiel erst einmal nur in einer Region oder für eine Benutzergruppe. Verhalten sich die neuen Versionen dann stabil, kann dann das Release weiter ausgerollt werden.
Solche kontrollierten Freigaben und Experimente auf Produktion machen Integrationstests nicht überflüssig, helfen aber zusammen mit den API- und Contract-Tests Integrations- und E2E-Tests sinnvoll zu ergänzen.
Dabei sollte aber immer beachtet werden, dass die Deployment-Pipelines möglichst effizient bleiben sollten. Ein Beispiel dafür ist der Einsatz von Mocks: In der Regel sollten vor allem in Unit- und Servicetests Abhängigkeiten durch Mocks ersetzt werden, um die benötigte Stabilität und Schnelligkeit dieser Tests gewährleisten zu können. Da Microservices im Sinne einer funktionalen Dekomposition in der Regel aber für ein Feature komplett verantwortlich sind, kapseln sie oft Abhängigkeiten zu Infrastrukturkomponenten. Ist dies der Fall, stellt sich die Frage, ob diese Infrastrukturkomponenten durch Mocks ersetzt werden sollten. Bei „sociable“ Tests werden im Gegensatz zu „solitary“ Tests bewusst solche Abhängigkeiten mitgetestet. Die eigentlichen Integrationstests können sich dann auf die Integration der Dienste fokussieren.
CI/CD-Deployment-Pipelines – Flaschenhälse vermeiden
Wir haben in diesem Artikel eine CI/CD-Deployment-Pipeline so umgestellt, dass die Integration effizient getestet werden kann. Dabei hat sich gezeigt, dass es nicht nur um die Integration der verschiedenen Microservices geht. Es sollte nicht, wie in der zweiten Abbildung dargestellt, eine spezielle Integrationstestphase in der CI/CD geben – vielmehr findet Integration in allen Bereichen einer CI/ CD-Deployment-Pipeline statt:
- Continuous Integration (CI) ist die Integration in das Versionskontrollsystem.
- Bei „sociable“ Tests eines Microservice können dessen Abhängigkeiten mitgetestet werden, die von dem Microservice gekapselt werden.
- Integrationsszenarien mehrerer Dienste können mit CDC-Tests innerhalb der eigenen Deployment-Pipeline getestet werden.
- Die eigentlichen Integrationstests mit mehreren Diensten sowie E2E-Tests sollten sich auf die grundlegenden und wichtigsten Benutzungsszenarien beschränken.
- Deployment und Release umfasst die Integration in die Produktionsumgebung. Idealerweise erfolgt diese Integration schrittweise und kontrolliert.