Code-Beispiele mit Testinfra: So testen Sie Docker-Images und vermeiden böse Überraschungen in Containern.
Bei Anwendungen, die in Docker-Containern laufen, verlagert sich ein guter Teil des Infrastruktur- und auch des Applikationssetups in die Container. Dieser Part wird oft weder durch Infrastrukturtests noch durch die fachlichen Tests der Anwendung abgedeckt. Dies kann zu bösen Überraschungen führen, wenn ein Fehler im Docker-Image selbst begründet liegt. Die Fehlersuche gestaltet sich oft langwierig und schwierig.
Es ist jedoch durchaus möglich zu testen, ob Images und die aus ihnen erzeugten Container korrekt aufgebaut sind. Im Gegensatz zu Infrastrukturtests, welche den tatsächlichen Status laufender Infrastruktur testen, kann ein Entwickler auf diese Weise Tests bereits zur Entwicklungszeit ausführen. Ebenso können die Tests in den Build-Prozess eines Images integriert werden, und so Regressionen vermeiden.
Damit schließt sich die Lücke zwischen den fachlichen Tests einer containerisierten Anwendung und der Infrastruktur, auf der diese Anwendung läuft.
In diesem Artikel zeige ich anhand von konkreten Code-Beispielen, wie Sie solche Tests mit Hilfe von Testinfra schreiben können. Sie werden lernen, wie Sie
- Tests gegen einen laufenden Container ausführen,
- Images zur Test-Zeit bauen und Container programmatisch starten
- sowie Images testgetrieben entwickeln.
Als Beispiel dient ein Team aus Entwicklerinnen und Entwicklern, welches das offizielle Jenkins-Image testen und an eigene Bedürfnisse anpassen möchte.
Einen laufenden Container testen
Im offiziellen Jenkins-Container startet ein Java-Prozess unter dem Benutzer jenkins
. Unser Beispiel-Team möchte sicherstellen, dass es dieses grundlegende Setup nicht durch eigene Anpassungen gefährdet. Das Team nutzt dazu Testinfra , ein Tool zum Testen von Infrastruktur. Es basiert auf der Programmiersprache Python und dem Test-Framework pytest .
Eine Entwicklerin des Teams schreibt die folgenden beiden Testinfra-Tests:
1# ./tests/image_test.py
2import pytest
3
4def test_current_user_is_jenkins(host):
5 assert host.user().name == "jenkins"
6 assert host.user().group == "jenkins"
7
8def test_jenkins_is_running(host):
9 jenkins = host.process.get(comm="java")
10 assert jenkins.args == "java -jar /usr/share/jenkins/jenkins.war"
11 assert jenkins.user == "jenkins"
Der erste Test stellt sicher, dass der Benutzer jenkins
existiert und dass er als aktueller Benutzer gesetzt ist.
Der zweite Test gewährleistet, dass der Jenkins-Prozess läuft und die korrekte Benutzerkennung hat. Die Tests stellen also sicher, dass das Jenkins-Image sich wie erwartet verhält, und dieses Verhalten durch spätere Anpassungen nicht beeinträchtigt wird.
Die Tests setzen voraus, dass ein Jenkins-Container läuft. Der folgende Befehl startet diesen:
1docker run --rm --name=jenkins_test_container jenkins
Anschließend startet testinfra
mit dem Connection-Backend docker
die Tests. Mit dem --hosts
Parameter kann dabei definiert werden, gegen welchen Container die Tests ausgeführt werden:
1testinfra --connection docker --hosts=jenkins_test_container
Alternativ kann der zu testende Container über die Variable testinfra_hosts
im Test-Modul angegeben werden:
1import pytest 2 3testinfra_hosts = ["docker://jenkins_test_container"]
Dadurch ist es möglich, verschiedene Container in unterschiedlichen Modulen zu testen. Die Tests können dann einfach mit dem Kommando testinfra
oder pytest
gestartet werden.
Container bauen und testen
Praktischer ist es, wenn das Test-Modul den Container programmatisch startet. Dies geht mit Hilfe der Bibliothek docker-py problemlos:
1#./tests/image_test.py
2import pytest
3import docker
4
5testinfra_hosts = ["docker://jenkins_test_container"]
6
7@pytest.fixture(scope="module", autouse=True)
8def container():
9 client = docker.from_env()
10 container = client.containers.run('jenkins', name="jenkins_test_container", detach=True)
11 yield container
12 container.remove(force=True)
Diese Codezeilen definieren ein Test-Fixture , das vor den Tests einen Container startet und diesen im Nachgang wieder entfernt. Der Parameter scope="module"
sorgt dafür, dass nur ein Container für das gesamte Modul gestartet wird. Andernfalls würde jeder einzelne Test einen Container erzeugen und wieder entfernen. autouse=True
bewirkt, dass das Fixture erzeugt wird, obwohl die Tests es nicht explizit referenzieren.
Das Team hat mit dieser Test-Suite nun ein gutes Fundament, um ein angepasstes Jenkins-Image zu testen. Einer der Entwickler scheibt folgendes Dockerfile, um ein neues Image auf Basis von jenkins
zu definieren:
1# ./src/Dockerfile 2FROM jenkins 3MAINTAINER John Doe <john.doe@example.com>
Das Beispiel nimmt noch keine Anpassungen vor. Im ersten Schritt sollen die bestehenden Tests gegen dieses neue Image laufen. Die Bibliothek docker-py kann das Image zur Laufzeit der Tests bauen. Ein Session-scope Fixture in der Datei conftest.py
baut das Image einmalig – vor allen Tests des Projekts:
1#./tests/conftest.py
2import docker
3import pytest
4
5@pytest.fixture(scope="session")
6def client():
7 return docker.from_env()
8
9@pytest.fixture(scope="session")
10def image(client):
11 return client.images.build(path='./src')
Das erste Fixture erzeugt zunächst einen Docker-Client, der im zweiten Fixture das Image baut. Die build
-Funktion greift dazu auf das Dockerfile im src
-Ordner zu.
Das container
-Fixture aus dem Test-Modul muss nun natürlich das frisch gebaute Image verwenden:
1# ./tests/image_test.py
2@pytest.fixture(scope="module", autouse=True)
3def container(client, image):
4 container = client.containers.run(image.id, name="jenkins_test_container", detach=True)
5 yield container
6 container.remove(force=True)
Die beiden Fixtures aus conftest.py
werden per Namenskonvention in das container
-Fixture hinein gereicht. Der Docker-Client startet dort nun einen Container auf Grundlage des zuvor gebauten Images.
Testgetriebene Container-Entwicklung
Die Entwicklerin beginnt nun damit, die Funktionen des Jenkins-Image zu erweitern.
Ganz im Sinne testgetriebener Entwicklung, kann sie die Anforderungen in Form eines Tests abbilden:
1# ./tests/image_test.py
2def test_maven_is_installed(host):
3 assert host.package("maven").is_installed
Da das Team Maven-Projekte bauen möchte, muss Maven installiert sein. Der Test prüft mittels host.package
, ob das maven
Paket installiert ist. Intern wird dazu die Paketverwaltung des Images verwendet. In diesem Beipiel ist es dpkg. Weil das Jenkins-Image die Anforderung noch nicht erfüllt, schlagen die Tests fehl. Testinfra gibt die Fehlermeldung aus, die im Container aufgetreten ist:
1Failed: Unexpected exit code 1 for CommandResult(command="dpkg-query -f '${Status}' -W maven", exit_status=1, stdout=None, stderr='dpkg-query: no packages found matching maven\n')
Um dies zu korrigieren, wechselt das Dockerfile per USER
-Instruktion zum root
Benutzer und installiert anschließend maven
:
1# ./src/Dockerfile 2USER root 3RUN apt update && apt install -y maven
Der neue Test wird jetzt grün, jedoch schlagen die anderen beiden Tests fehl. Das Sicherheitsnetz hat funktioniert und warnt davor, dass nun der Benutzer root
Jenkins ausführt!
1def test_jenkins_is_running(host): 2 jenkins = host.process.get(comm="java") 3 assert jenkins.args == "java -jar /usr/share/jenkins/jenkins.war" 4> assert jenkins.user == "jenkins" 5E assert 'root' == 'jenkins' 6E - root 7E + jenkins
Eine weitere USER
-Instruktion nach der Installation korrigiert diesen Fauxpas:
1# ./src/Dockerfile 2USER root 3RUN apt update && apt install -y maven 4USER jenkins
Nun laufen alle Tests erfolgreich durch!
1tests/image_test.py::test_current_user_is_jenkins[docker://jenkins_test_container] PASSED 2tests/image_test.py::test_jenkins_is_running[docker://jenkins_test_container] PASSED 3tests/image_test.py::test_maven_is_installed[docker://jenkins_test_container] PASSED 4 5================================== 3 passed in 3.31 seconds ==================================
Ausblick
Mit den gezeigten Beispielen hat das Team ein gutes Basis-Setup und kann nach Bedarf weitere Tests schreiben und das Image an die Anforderungen anpassen. Testinfra ist ein geeignetes Werkzeug, um die Änderungen mit Tests abuzusichern.
Es ist auch möglich eine Reihe von Container-Konfigurationen zu testen, indem verschiedene Test-Module unterschiedliche Container-Fixtures nutzen. Dazu kann docker-py zum Beispiel unterschiedliche Umgebungsvariablen setzen, welche den Aufbau des Containers beeinflussen.
Dabei sollten Test-Autoren jedoch berherzigen, das Verhalten des Dockerfiles und nicht das der containerisierten Anwendung zu testen.
Auch komplexe Setups mit untereinander verlinkten Containern sind denkbar. Zum Beispiel könnte docker-py Mock-Container starten, um den zu testenden Container in eine kontrollierte Umgebung zu isolieren.
Fazit
Mit Testinfra können Teams nicht nur Infrastruktur testen. Auch für Docker-Images ist es ein geeignetes Werkzeug. Tests können mit docker-py Images zur Laufzeit bauen und Container starten. Testinfra bietet eine Reihe von Modulen, die den Aufbau dieser Container überprüfen können. Darüber hinaus stehen Entwicklerinnen und Entwicklern alle Möglichkeiten von pytest offen.
Die Beispiele aus diesem Artikel sind auf Github verfügbar.
Exkurs: Testinfra im Container ausführen
Wenn man ohnehin mit Docker arbeitet liegt es nahe, die Tests ebenfalls im Container auszuführen. Das Image [aveltens/docker-testinfra
] enthält alle dazu nötigen Abhängigkeiten. Der Source-Code des Projekts und der Docker-Socket des Hostsystems müssen lediglich über ein Volume in den Container einghängt werden:1docker run --rm -t \ 2 -v $(pwd):/project \ 3 -v /var/run/docker.sock:/var/run/docker.sock:ro \ 4 aveltens/docker-testinfra
Weitere Beiträge
von Angelo Veltens
Dein Job bei codecentric?
Jobs
Agile Developer und Consultant (w/d/m)
Alle Standorte
Weitere Artikel in diesem Themenbereich
Entdecke spannende weiterführende Themen und lass dich von der codecentric Welt inspirieren.
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.
Blog-Autor*in
Angelo Veltens
Software Developer & Consultant
Du hast noch Fragen zu diesem Thema? Dann sprich mich einfach an.
Du hast noch Fragen zu diesem Thema? Dann sprich mich einfach an.