Qualität und Stabilität sind wichtige Eigenschaften in der Software-Entwicklung. Je weiter die Entwicklung von Software voranschreitet, desto mehr muss auf diese beiden Faktoren geachtet werden. Dieser Artikel dreht sich um die Qualitätssicherung von Software mittels kontinuierlicher Integration mit Jenkins.
Neben der Funktionalität von Anwendungen ist es wichtig, dass Software sowohl stabil als auch zuverlässig läuft. Im Laufe der Zeit steigt nicht nur die Funktionalität von Programmen, sondern auch die Anzahl möglicher Fehlerquellen mit den hinzukommenden Codezeilen an. Möglichst viele Fehler zu finden und zu korrigieren ist ein wichtiger Aspekt von qualitativ hochwertiger Software. Das Testen und das Finden von Fehlern kann nur in begrenzten Maßen händisch erledigt werden.
Modultests
Software-Anwendungen sind in der Regel in einzelne Module, also abgeschlossene Einheiten, unterteilt. Um die Funktionalität von Anwendungen zu testen, werden daher Modultests geschrieben, welche dann in regelmäßigen Abständen automatisiert ausgeführt werden. Die Modultests werden besonders im englischsprachigen Raum auch häufig Unit Tests genannt. Modultests können dabei für verschiedene Schnittstellen geschrieben werden. Die Art der Implementierung hängt hingegen von der Software ab. In der Regel gibt es eine Gruppe an Modultests, welche die direkte Funktion von Methoden auswertet. Ein einzelner Test prüft dabei beispielsweise, ob die Methode genau das zurückliefert, was erwartet wird. Dieses Verhalten wird dann mit einigen beispielhaften Daten getestet.
Als Beispiel für einen Modultest kann man sich folgendes Einsatzszenario vorstellen: Das Objekt eines Feedgenerators wird mit Daten gefüllt, zum Beispiel mit Artikeln aus einem Blog. Beim Aufruf einer Methode werden die Daten in den RSS-Feed als XML-Datei geschrieben. Hierbei müssen mehrere Problemfälle überprüft werden: Zum einen, ob alle eingegebenen Daten auch den Erwartungen entsprechend in die XML-Datei geschrieben worden sind. Des Weiteren sollte überprüft werden, ob die entsprechende XML-Datei auch valide und wohlgeformt ist – also den Regeln entsprechen. Die Überprüfungen übernimmt dabei ein Test-Framework.
Das vorangegangene Beispiel testete die direkte Funktionsweise einer Methode, dies ist allerdings nicht die einzige Schnittstelle, die bedient werden kann. Je nach Anwendung gibt es weitere Schnittstellen, deren Test durchaus sinnvoll ist. Geht man beispielsweise von einer GUI-Anwendung aus, reicht es es nicht unbedingt aus, nur die Methoden einer Klasse zu testen. So sollte auch die graphische Oberfläche getestet werden. Bei einem Editor könnte man so testen, ob die „Rückgängig machen“-Funktion auch wirklich das ausführt, was erwartet wird. Beim Programmieren von Webseiten kann man testen, ob die integrierte Suchfunktion auch ordnungsgemäß funktioniert. Den Suchindex kann man während des Tests mit Testdaten füttern, nach welchen dann im Anschluss gesucht wird. Schlägt da etwas fehl, fällt es frühzeitig auf.
Bei Modultests ist es wichtig, dass diese automatisiert ausgeführt und ausgewertet werden. Dies hängt auch mit dem Konzept der kontinuierlichen Integration zusammen.
Kontinuierliche Integration
Bei der kontinuierlichen Integration handelt es sich um das stetige Hinzufügen von neuem Entwicklungsquellcode zu einem Projekt. Entwickler nutzen dabei ein Versionsverwaltungssystem für den Quellcode. Darunter fallen beispielsweise Git, Subversion oder auch das in Launchpad genutzte Bazaar.
Das Konzept der kontinuierlichen Integration besteht aus zwei Grundsätzen, nämlich der Integration sowie dessen Kontinuität. Unter Integration versteht man das Einfügen von neuem oder geändertem Programmcode in das Ursprungsprojekt. Die Kontinuität der Integration erfolgt dabei in vielen kurzen Abständen. Die Häufigkeit unterscheidet sich oft von Projekt zu Projekt, in der Regel werden aber mindestens einmal am Tag Änderungen vorgenommen. Wichtig ist, dass viele kleine Änderungen übernommen werden können. Der Vorteil liegt dabei in dem frühzeitigen Erkennen von Fehlern im Programmcode – insbesondere bei der Verwendung von Modultests.
Der aktuelle Stand der Entwicklung wird dabei mit Hilfe einer Software für kontinuierliche Integration getestet. Darunter fällt zunächst einmal das Kompilieren des Projekts sowie im Anschluss das Durchführen der geschriebenen Modultests.
Jenkins
Um ein Software-Projekt mit Hilfe von kontinuierlicher Integration zu entwickeln, gibt es viele Software-Produkte, die den Entwickler dabei unterstützen. Darunter fällt Jenkins, welches in diesem Artikel betrachtet wird. Jenkins wurde unter dem Dach von Sun Microsystems von Kohsuke Kawaguchi entwickelt. Damals hieß es „Hudson“. Kawaguchi verließ das Unternehmen, nachdem Sun von Oracle übernommen wurde. Oracle verweigerte die weitere Nutzung des Namens „Hudson“ und entwickelt es selbst weiter. Da Kawaguchi Hudson unter dem Namen „Jenkins“ weiterentwickelt, ist es somit ein Fork. Jenkins ist in Java geschrieben und läuft plattformunabhängig als Web-Anwendung auf einem Server. Der Quellcode unterliegt der MIT-Lizenz.
In Jenkins können Jobs definiert werden, die als sich wiederholende Arbeitsabläufe zu verstehen sind. In der Regel umfasst dies mehrere Schritte, die automatisiert ausgeführt werden. Innerhalb der Konfiguration eines Jenkins-Jobs ist es möglich, verschiedene Arbeitsabläufe zu definieren, die unter Linux in der Regel Shell-Skripte enthalten. Jenkins kann man daher auch als Aufsatz für derartige Skripte verstehen. Der Vorteil von Jenkins ist, dass sich die Jobs leicht und intuitiv erstellen lassen und die Entwickler zudem viele Konfigurationsmöglichkeiten nutzen können.
Generell werden Jenkins-Jobs in zwei verschiedenen Varianten ausgeführt. Die erste ist das Konzept der täglichen Builds, die täglich ausgeführt werden. Das zweite Konzept ist die bereits erwähnte kontinuierliche Integration von Quellcode, die immer dann ausgeführt wird, wenn Änderungen am Quellcode vorgenommen worden sind. Hierbei wird ein Jenkins-Job immer dann gestartet, wenn Änderungen im Projektarchiv durchgeführt wurden. Bei den täglichen Builds hingegen werden die Änderungen eines Tages stets zusammengefasst.
Ein Jenkins-Job bildet einen gewissen Arbeitsablauf ab, welchen man in vier Unterpunkte aufteilen kann. Die erste Ausführung ist die Auslösung des Jobs. Dieser kann zeitgesteuert, ereignisgesteuert oder durch eine Änderung im Quellcode ausgelöst werden. Bei der zeitgesteuerten Auslösung kann eine bestimmte Uhrzeit definiert werden. Wenn zum Beispiel 18 Uhr angegeben wurde, dann startet Jenkins automatisch den angelegten Job. Äquivalent dazu kann auch ein Job ereignisgesteuert ausgelöst werden. Dabei kann konfiguriert werden, dass ein bestimmter Jenkins-Jobs nur dann ausgeführt werden soll, wenn ein Vorgänger-Projekt erfolgreich verlaufen ist. Die dritte Möglichkeit ist die Auslösung nach einer Änderung im Quellcode. Jenkins scannt dazu das Quellcode-Archiv (Repository) in gleichmäßigen Abständen und löst den Jenkins-Job aus, wenn eine Änderung registriert wurde. Der letzte Auslöser ist der einfachste: die manuelle Ausführung.
Der zweite Schritt ist ein denkbar kurzer: Der Jenkins-Jobs startet, lädt sich den Quellcode vom Repository herunter und geht dann in den dritten Schritt über, den Buildvorgang.
%HD: “Skripte” geandert zu “Skripten” Der Buildvorgang kann sehr individuell genutzt werden, da er in Shell- beziehungsweise Windows-Batch-Skripten spezifiziert wird. Als Nutzer von Jenkins hat man dabei sehr umfangreiche Möglichkeiten, den Buildvorgang zu gestalten. Generell wird im Buildvorgang das Projekt kompiliert sowie die Tests ausgeführt. Sofern bei den jeweiligen definierten Shell-Skripten keine gravierende Fehler vorkommen – zum Beispiel Programmabstürze – geht Jenkins in den vierten Schritt über. Wenn jedoch etwas schief geht, dann bricht der Buildvorgang komplett ab und meldet den Entwicklern den Fehlschlag des Buildvorgangs.
Der Post-Buildvorgang ist der vierte und letzte Schritt, welchen Jenkins durchführt. Hierbei handelt es sich um Aktionen, die alle nach dem Buildvorgang durchgeführt werden. Dort können dann ebenfalls einige Aktionen definiert werden. So ist es sinnvoll, das Projekt in ein Paket zu packen. Je nach verwendetem System ist es möglich, sofort ein fertiges DEB- oder RPM-Paket bauen zu lassen. Daneben werden im Post-Buildvorgang auch die ausgeführten Tests ausgewertet. Die Modultests schreiben die Ergebnisse der durchgeführten Tests in Log-Dateien. Die Art der Log-Dateien unterscheidet sich dabei vom eingesetzten Test-Framework. Von Haus aus unterstützt Jenkins das Java-Test-Framework JUnit. Häufig werden hierzu XML-Dateien genutzt, die der Jenkins-Job zum Schluss auswertet. Bei der Auswertung werden dann die Log-Dateien nach einem festgelegten Schema eingelesen und interpretiert. Dabei wird die Anzahl der fehlgeschlagenen Modultests gezählt und als Ergebnis des Jenkins-Jobs ausgegeben.
Anschließend erzeugt Jenkins mit dem Testergebnissen den Status des aktuellen Builds in Form von Farben und einen sogenannten „Wetterbericht“. Der Status ist „rot“, sobald zu viele Fehler aufgetreten sind, „gelb”, wenn eine geringe Fehleranzahl aufgetreten ist, und „blau“, sofern keiner der Modultests fehlgeschlagen ist. Häufig werden alternativ auch die „Ampel-Farben“ genutzt, so dass ein erfolgreicher Build mit Grün statt Blau gekennzeichnet ist. Was unter einer „geringen Anzahl der Fehler“ verstanden werden soll, lässt sich dabei einstellen. Wenn ein Jenkins-Jobs mehrmals durchgelaufen ist – der Quellcode also mehrfach Veränderungen erfahren hat – wird daraus der Wetterbericht erstellt. Der Wetterbericht zeigt an, wie der Verlauf der letzten fünf Jenkins-Jobs war. Strahlenden Sonnenschein gibt es, sofern alle Builds erfolgreich durchgeführt werden konnten, wohingegen es Gewitter gibt, wenn alle letztmaligen Builds fehlgeschlagen sind. Zudem existieren noch weitere Zustände wie zum Beispiel Wolken, sofern nur wenige der letzten Vorgänge fehlgeschlagen sind. Die einzelnen Werte, aus denen der Status eines Jenkins-Jobs erzeugt wird, lassen sich hierbei ebenfalls konfigurieren, sodass Entwickler-Teams die volle Kontrolle über ihre Jenkins-Jobs haben.
Zum Abschluss des Post-Buildvorgangs und somit des gesamten Durchlaufs eines Jenkins-Jobs müssen schließlich noch die Entwickler informiert werden. Entwickler können entweder die E-Mail-Benachrichtigung nutzen oder alternativ sich des großen Plug-in-Pools bedienen. So können die Entwickler durch das Nutzen von Plug-ins über das XMPP-Protokoll oder einem IRC-Bot informiert werden. Wenn man noch einen weiteren Jenkins-Job direkt im Anschluss ausführen will, kann man zudem einen Trigger setzen, welcher dann den anderen Jenkins-Job startet.
Plug-ins
In der Standardvariante bietet Jenkins bereits eine breite Palette an Funktionen, die den Entwicklern die kontinuierliche Integration etwas erleichtert. Durch die zahlreich verfügbaren Erweiterungen ist es möglich, den Funktionsumfang von Jenkins deutlich zu vergrößern. Da Jenkins hauptsächlich Werkzeuge mitbringt, die für Java-Entwickler interessant sind, wie etwa die Auswertung von JUnit-Tests, gibt es ebenfalls Plug-ins, die für Entwickler anderer Programmiersprachen interessant sind. Für C++-Entwickler ergibt sich dadurch die Möglichkeit, ebenfalls die Vorzüge von Jenkins kennen zu lernen. So unterstützt Jenkins durch Plug-ins beispielweise das Boost-Test-Framework, welches zu den C++-Boost-Libraries gehört. Ebenfalls ist es möglich, Tools wie cppcheck laufen zu lassen, um C++-Code-Analysen durchzuführen. Interessant ist auch die Möglichkeit, eine Dokumentation generieren zu lassen, sodass täglich neue Dokumentationen zur Verfügung stehen. Eine lange Plug-In-Liste findet sich im Jenkins-Wiki.
Öffentliche Jenkins-Server
Da Jenkins eine Web-Applikation ist, gibt es von einigen Projekten öffentlich einsehbare Jenkins-Server. Solche öffentlichen Jenkins-Server gibt es mindestens von zwei großen OSS-Projekten, z.B. Ubuntu und KDE. Ein Blick für Interessierte lohnt sich beispielsweise beim Jenkins-Job „akonadi_master“ von KDE. Dort sieht man beispielsweise den Build-Verlauf der letzten Wochen beziehungsweise Monate. Weiterhin sieht man einige weitere Graphen, die unter anderem die Testergebnisse darstellen oder auch die Warnungen des GNU Compilers skizzieren.
Fazit
Als Entwickler hat man bei der kontinuierlichen Integration mit Jenkins diverse Vorteile. Der Quellcode wird regelmäßig in kurzen Abständen in das Projekt eingepflegt und sowohl grobe als auch kleine Fehler fallen zügig auf, sofern ausreichend qualitativ und quantitativ gute Testfälle geschrieben werden. Jenkins erleichtert Entwicklern den Überblick über mögliche Fehler und unterstützt die Qualitätssicherung durch eine breite Funktionspalette.
Neben Jenkins gibt es natürlich auch noch alternative Software für kontinuierliche Integration, die man statt Jenkins nutzen kann. Hierzu zählen beispielsweise Travis CI für die Open Source Community, Apache Gump oder auch BuildBot.