Microservices-Architekturen, aber auch klassische Softwaresysteme sind auf die reibungslose Zusammenarbeit aller Komponenten angewiesen. Das schließt in der Regel auch Infrastrukturdienste wie Datenbanken, Messaging-Systeme oder HTTP-APIs mit ein. Entwicklerinnen und Entwickler sollten daher bei ihrer täglichen Arbeit nicht auf Integrationstests verzichten. Eine geeignete Testumgebung mit Docker-Containern lässt sich mit Testcontainers oder Docker Compose unkompliziert auch lokal aufsetzen und steuern.
Damit sich die Tests nicht negativ auf die Produktivität auswirken und den Entwicklungsflow unterbrechen, sollten sie möglichst schnell durchlaufen. Auf typischer Notebook-Hardware können bei Integrationstests aber schnell viele Minuten vergehen, bis alles kompiliert ist, alle Container gestartet, mit Testdaten befüllt und die Tests abgeschlossen sind. Wird die CPU darüber hinaus durch parallel laufende Prozesse wie dem für kollaboratives Remote Mob Programming notwendigen Screensharing zusätzlich belastet, gelangen auch leistungsstarke Notebooks schnell an ihre Grenzen. Auf Nicht-Linux-Systemen kommt verschärfend hinzu, dass Docker nur in einer virtuellen Umgebung mit limitierten Ressourcen läuft.
Es gibt jedoch einen einfachen Weg, die Tests zu beschleunigen: Die Docker Engine lässt sich separat auf einem leistungsfähigen Rechner installieren, der als Docker-Server dient. Testcontainers greift dann über die Docker-REST-API via HTTPS auf den Docker Daemon zu. Auf diese Weise lassen sich Container auf dem Server ausführen und sind dort über entsprechend gemappte Ports erreichbar. Der lokale Arbeitsplatzrechner ist entlastet und seine Kapazitäten stehen für die eigentlichen Tests bereit.
Installation des Docker-Servers
Das Einrichten und Konfigurieren eines Docker-Servers soll im Folgenden am Beispiel eines lokalen Systems gezeigt werden. Alternativ wäre auch ein vergleichbarer Cloud-Server denkbar. Der Rechner verfügt über vier CPU-Kerne, acht GByte RAM und ist mit einer SSD ausgestattet. Als Betriebssystem kommt die aktuelle LTS-Version von Ubuntu, 20.04 LTS x64, zum Einsatz. Linux eignet sich für die Aufgabe grundsätzlich besonders gut, da es Docker nativ unterstützt.
Um Docker auf Ubuntu zu installieren, sind dennoch eine Reihe von Befehlen erforderlich:
Docker HTTPS Endpoint aktivieren
Der Docker Daemon unterstützt verschiedene Sockets, um per Docker Engine API auf die Docker Engine zuzugreifen.
Standardmäßig kommuniziert der Docker Client auf dem lokalen Unix IPC Socket unix:///var/run/docker.sock
mit dem Docker Daemon – alternativ lässt sich auch ein reguläres TCP Socket verwenden.
Grundsätzlich würde es genügen, den Docker Daemon dockerd
auf den TCP Port 2375 zu binden:
Dieser Ansatz ist jedoch zu unsicher, da er einen unverschlüsselten und unautorisierten Zugriff auf den Docker Host und damit letztlich auf den root-Account des Betriebssystems erlaubt. Entwicklerinnen und Entwickler sollten den Zugriff daher per TLS-Client-Zertifikat absichern. Dazu sind auf dem Server die in Listing 2 aufgeführten OpenSSL-Befehle auszuführen, um Private-Keys und Zertifikate zu erstellen:
Anschließend lässt sich TCP für den Docker Daemon aktivieren – in Ubuntu üblicherweise in systemd
. Nach dem Start von
lässt sich im geöffneten Editor folgender Eintrag setzen:
Nach einem Neustart mit
akzeptiert der Docker Daemon zertifizierte Connections auf dem Port 2376.
Achtung: Auch wenn der Zugriff auf den Docker-Host nun gesichert erfolgt, sind die durch Container geöffneten Ports gegebenenfalls öffentlich erreichbar. Der Server sollte daher in jedem Fall hinter einer Firewall liegen beziehungsweise nur über ein VPN erreichbar sein.
Client-Konfiguration
Zum Konfigurieren des Clients lassen sich nun der Client-Private-Key und das CA-Zertifikat auf den lokalen Rechner in das Verzeichnis ~/.docker/ installieren – beispielsweise durch Kopieren vom Docker Host per SCP. Dabei sind die erreichbare IP-Adresse beziehungsweise der Hostname des Docker-Hosts anzugeben:
Damit der Docker-Client weiß, welchen Docker-Host er ansprechen soll, ist in der Run Configuration je nach verwendeter Shell in ~/.zshrc oder ~/.bashrc noch die Umgebungsvariable DOCKER_HOST
zu setzen.
Zudem muss DOCKER_TLS_VERIFY
signalisieren, dass TLS zum Einsatz kommt:
Anschließend lassen sich einem neuen Terminal-Fenster sämtliche docker-Befehle an den Docker-Server weiterreichen:
Verwenden des entfernten Docker-Hosts in Testcontainers
Testcontainers erkennt automatisch die Umgebungsvariable DOCKER_HOST
– zusätzliche Konfigurationsschritte sind daher nicht notwendig.
Zu prüfen ist jedoch, dass in den Tests die Container-Dienste nicht über localhost
konfiguriert sind, sondern über die von den Testcontainern bereitgestellten Methoden.
Listing 3 zeigt ein Spring-Boot-Beispiel zum Verwenden eines MongoDB-Testcontainers, der die korrekte ReplicaSet-URL liefert:
Geschwindigkeitsvergleich
Um einen Eindruck von den erzielbaren Geschwindigkeitsvorteilen zu erhalten, dient ein realitätsnahes Spring-Boot-Projekt mit 224 Tests als Basis für einen Vergleich. Die Testkonfiguration für JUnit-Tests in einer Spring-Boot-Anwendung mit mehreren Spring Contexts enthält Testcontainer für MongoDB, Kafka und MockServer. Die verwendeten Server sind mit vier CPU-Kernen und acht GByte RAM ausgestattet. Der Vergleich liefert die jeweilige Testdurchführungsdauer für einen Docker-Host auf einem MacBook Pro (13 Zoll), einem dedizierten Server im gleichen Netzwerk und einer Cloud-Instanz. Um dabei sicherzustellen, dass alle Docker-Images lokal gecacht vorliegen, erfolgten sämtliche Testdurchläufe mehrfach.
Docker Host | Testdauer (in Minuten) |
---|---|
Docker for Mac | 4:53 |
Lokaler Server | 2:53 |
AWS EC2 c5.xlarge | 5:14 |
Der Geschwindigkeitsvorteil des lokalen Servers gegenüber dem Arbeitsplatzrechner beträgt zwei Minuten, was sich im täglichen Gebrauch enorm positiv auswirkt. Auf einen typischen Arbeitstag hochgerechnet können schnell 30 Minuten oder mehr zusammenkommen.
Das überraschend schlechte Abschneiden der EC2-Cloud-Instanz lässt sich vermutlich auf die deutlich höhere Netzwerklatenz zurückführen, da sowohl die Tests wie auch die Anwendung intensiv mit den Containern interagieren.
Fazit
Sorgfältige Integrationstests sind essenziell für den Erfolg von Software-Projekten. Testcontainers empfehlen sich dabei als exzellenter Weg, um die benötigten Infrastrukturdienste mit Docker-Containern aufzusetzen.
Dauern die Tests allerdings zu lange, leidet darunter die Gesamtproduktivität des Projekts, und Entwicklerinnen und Entwickler tendieren schlimmstenfalls dazu, auf das Schreiben weiterer Integrationstests zu verzichten.
Es lohnt sich daher in die Optimierung der Testdurchführungsdauer zu investieren. Ein separater, leistungsfähiger Server zum Ausführen der Docker-Container ist ein einfacher Weg, um die Geschwindigkeit der Tests signifikant zu verbessern.