Continuous Integration in Zeiten agiler Programmierung

Seite 2: CI-Server

Inhaltsverzeichnis

Da die Ideen von Continuous Integration seit Langem bekannt und etabliert sind, hat sich mittlerweile eine große Landschaft verfügbarer CI-Server gebildet. Doch woran macht sich die Auswahl eines konkreten CI-Servers fest? Das kann in der Praxis von mehreren Kriterien abhängen:

  1. Kompatibilität mit vorhandener Infrastruktur: Der eigentliche CI-Server ist nur Teil einer größeren Entwicklungsinfrastruktur. Insbesondere sollte zum Beispiel die Interoperabilität mit einem vorhanden VCS oder einem etablierten Build-Tool gewährleistet sein.
  2. Optimierung für bestimmte Szenarien: Manche CI-Server sind speziell auf bestimmte Programmiersprachen, Frameworks oder integrierte Entwicklungsumgebungen (IDEs) hin optimiert, sodass das unter Umständen ein wichtiges Entscheidungsmerkmal ist.
  3. Funktionsumfang und Erweiterbarkeit: Ein weiteres wichtiges Kriterium ist sicherlich, welche Features ein CI-Server jenseits der weiter oben geschilderten "Grundfunktionen" bietet. Eng verknüpft damit ist die Frage, ob sich ein CI-Server von dritter Seite funktional erweitern lässt, etwa durch eine Plug-in-API.
  4. Proprietär oder Open Source: Auf dem CI-Server-Markt existieren sowohl proprietäre als auch Open-Source-Produkte. Auch das kann Einfluss auf die Entscheidungsfindung haben, zum Beispiel hinsichtlich Fragen rund um Lizenzkosten und Support oder ob großer Wert auf das Vorhandensein einer aktiven Community gelegt wird, die Plug-ins und Mailinglisten bietet.

Angesichts der Menge der verfügbaren CI-Server und der bei der Entscheidung zu berücksichtigenden Aspekte ist klar, dass sich keine "one size fits all"-Empfehlung geben lässt. Als Beispiel sei trotzdem Jenkins genannt, ein kostenfreier CI-Server, der sich derzeit großer Beliebtheit erfreut. Jenkins ist ein in Java programmierter Open-Source-CI-Server, der als Fork des CI-Servers Hudson hervorgegangen ist. Für die Hintergründe der Abspaltung und die daraus resultierenden Spannungen sei an dieser Stelle lediglich auf den Wikipedia-Beitrag zum Thema verwiesen.

Jenkins/Hudson war zwar ursprünglich primär für Java-Projekte gedacht, die gut integrierte Plug-in-Schnittstelle hat jedoch früh auch viele Entwickler anderer Programmiersprachen angezogen. Das hat zu zahlreichen Erweiterungen geführt, sodass derzeit viele gängige Build-Tools und Sprachen, auch aus dem Nicht-Java Umfeld, unterstützt werden. Insbesondere erfordert die Installation mittlerweile auch keine Java-Kenntnisse mehr, da fertig verwendbare Installer für die meisten Betriebssysteme verfügbar sind. Weitere Informationen, Dokumentation und Downloads finden sich auf der Jenkins-Website.

Heute nimmt der CI-Server nicht mehr nur die zuvor geschilderte Rolle des CI-Servers als reiner "Rechenknecht" ein, der lediglich Änderungen an der Code-Basis durch Kompilierung verifiziert. Vielmehr wird seine zentrale Rolle genutzt, um ein umfangreiches Qualitätsmanagement des gesamten Entwicklungsprojekts zu erreichen.

Das liegt auch nahe: Der CI-Server steuert mehr oder weniger den gesamten Build-Prozess und ist die zentrale Integrationsinstanz, die unabhängig von den einzelnen Entwicklern und der Projektsituation arbeitet und so als Quality Gate für das gesamte Projekt fungiert. In der Rolle ist er in der Lage, Aussagen zum Umfang und zur Qualität der Software zu treffen, den Entwicklungsstatus aufzuzeigen, zu historisieren und so über die Zeit zu betrachten. Der damit erlangte "Blick in das Entwicklungsprojekt" steht allen Projektbeteiligten zur Verfügung, die dadurch in der Lage sind, den Status selbst zu bewerten, frühzeitig auf Probleme zu reagieren und eine offene Kommunikation mit allen Beteiligten zu führen.

Um verlässliche Aussagen über den Zustand des Softwareprojekts und die Qualität des Programmcodes treffen zu können, muss der CI-Server Messgrößen wie den Umfang der Codebasis und die Stabilität des Entwicklungsprozesses ermitteln. Das können moderne CI-Server nur teilweise selbst, weswegen ein Entwickler hierfür normalerweise auf weitere Tools zurückgreifen muss.

Die gewonnenen Metriken lassen sich im nächsten Schritt nun beinahe beliebig dazu verwenden, Mindestqualitätsanforderungen zu überprüfen und bei Verletzungen entsprechend zu reagieren. Welche Anforderungen das sind, in welcher Phase des Build-Prozesses sie angewandt werden und in welchem Maße Verletzungen zu unmittelbaren Konsequenzen wie einem Scheitern des Builds führen, ist natürlich vor dem Hintergrund des konkreten Projekts zu entscheiden.

Beispiele für in der Praxis oft verwendete Messgrößen und die damit einhergehenden Fragestellungen sind:

  • Stil und formale Eigenschaften des Quellcodes: Gibt es nicht dokumentierte öffentliche Funktionen? Wird ein kritischer Wert für die Komplexität beziehungsweise der Umfang einer Funktion überschritten? Verletzt der Quellcode den Corporate Styleguide?
  • Testabdeckung: Decken Unit-Tests zentrale Teile des Programmcodes nicht ab? Werden Teile des Programmcodes nicht durch die Akzeptanztests erreicht und sind somit eventuell überflüssig?
  • Vorhandensein von Fehlermustern: Werden Variablen nicht korrekt initialisiert? Existiert identischer Quellcode mehrfach? Gibt es potenziell fehlerhafte Typumwandlungen? Erfolgt eine Zuweisung statt eines Vergleichs?
  • architekturelle Anforderungen: Existieren unerwünschte Abhängigkeiten zwischen Teilen des Quellcodes? Wie nah ist die Architektur an der Zielarchitektur?
  • Performance: Dauert die Request-Bearbeitung auf der physikalischen Zielinfrastruktur zu lange? Bricht die Performance bei zu vielen Nutzern ein? Erholt sich das System nicht nach Überlast?

Die Liste lässt sich deutlich erweitern und detaillieren und damit eine umfangreiche und individuelle Aussage über die Qualität des Softwareprojektes treffen.

Bei Trends handelt es sich hingegen um Momentaufnahmen. Oft spannender ist die Betrachtung über die Zeit, also ein Aufzeigen von Trends. Auch hierbei kommt dem CI-Server eine zentrale Rolle zu, denn mit den erfassten Messgrößen bietet er die Basis für eine Historisierung der Daten und damit beispielsweise der Beantwortung der folgenden Fragen: Steigt die Testabdeckung der Unit-Tests wie geplant? Welche Tests sind besonders instabil? Hat eine Änderung am Code zu einer ungewollten Änderung des Performanceverhaltens oder der benötigten Ressourcen geführt? Ändert sich die Stabilität der Hauptentwicklungslinie (Trunk) unverhältnismäßig, zum Beispiel durch die Vergrößerung des Projektteams?

Metriktrends am Beispiel der Sonar-Code-Quality-Plattform (Abb. 2)

Innerhalb eines komplexeren Szenarios unterscheiden sich die einzelnen Builds nicht nur in der Frage, ob sie erfolgreich waren oder nicht. Gerade die gemessenen Qualitätsmerkmale können unterschiedlich sein, und so ist es oft wünschenswert, einzelne Builds aufgrund bestimmter Eigenschaften automatisch oder manuell zu
kennzeichnen. CI-Server bieten dazu unterschiedliche Funktionen, beispielsweise das Anlegen eines Tags im VCS oder eine sogenannte "Promotion" des entsprechenden Builds. Letztere ermöglicht es etwa, gezielt einzelne Builds manuellen Tests oder weiterführenden Build-Schritten zuzuführen.

In der Theorie ist Continuous Integration die ideale Basis für die Entwicklung im Team und der CI-Server dabei das steuernde, überwachende Element des gesamten Entwicklungsprozesses. In der Praxis stellt sich dieses Ideal aber nicht immer ein: Der Build-Prozess dauert zu lange, die Tests sind zu instabil, der CI-Server meldet mehrmals täglich fehlgeschlagene Builds und wird sowieso als Fremdkörper wahrgenommen und gemessene Metriken interessieren eigentlich niemanden.

Gerade in unerfahrenen Teams oder in Projekten, in denen Qualität nicht oberstes Prinzip ist, stößt man auf solche oder ähnliche Situationen. Dem ist schnell und konsequent zu begegnen, indem das Vertrauen in den CI-Prozess, aber auch das Verantwortungsbewusstsein der Projektbeteiligten gestärkt wird. Wie das geht, zeigen mittlerweile etablierte Best Practices und Patterns (z. B. Paul M. Duvall: "Continuous Integration: Patterns and Anti-patterns"). Die Kerngedanken lassen sich wie folgt zusammenfassen.

Optimierung der Tests: Muss der Entwickler zu lange auf Feedback nach dem Check-in seine Codeänderung warten, verliert er nicht nur den aktuellen Fokus, sondern betrachtet den CI-Prozess als unproduktiv und überflüssig. Zu lang laufende Tests sind dabei meist die Ursache für dieses Problem. Dem lässt sich auf unterschiedliche Art begegnen: Erstens sollten die Tests passend strukturiert und partitioniert sein, sodass anlassbezogen beispielsweise nur schnelle Unit-Tests nach einem Commit für ein unmittelbares Entwicklerfeedback ausgeführt werden. Durch gutes Mocking externer Abhängigkeiten, aber auch fortgeschrittene Techniken wie Testpriorisierung und -selektion (z. B. Björn Feustel, Steffen Schluff: "Testest Du noch oder entwickelst Du schon (wieder)?" (PDF)) lässt sich zweitens die mittlere Ausführungszeit der Tests deutlich reduzieren. Schließlich ist es heute oftmals nicht nur der einfachste, sondern auch ein kostengünstiger Weg, die Testlaufzeiten zu optimieren, dem CI-Server leistungsstärkere Hardware zur Verfügung zu stellen oder die Tests verteilt über mehrere Serverinstanzen auszuführen.

Stabilisierung der Entwicklung: Schlägt der Build zu oft fehl oder wird er durch instabile Tests unberechenbar, schwindet das Vertrauen in den CI-Prozess und "rote" Builds werden nicht mehr als das wahrgenommen, was sie sind: große Produktivitätsstopper. Lösungen sind etwa

  • private Builds: Jeder Entwickler muss vor dem Commit seiner Änderungen einen lokalen Build samt der zugehörigen Tests ausführen, einschließlich einer Integration der Hauptentwicklungslinie mit diesen Änderungen. Das lässt sich beispielsweise durch leicht ausführbare Build-Skripte ("One Click Builds") oder durch automatisch ausgeführte Tests für das individuelle Commit (Pre-Tested Commit) unterstützen.
  • die Verantwortung für sogenannte Broken Builds: Sobald der CI-Server meldet, dass ein Build fehlschlug, ist das unmittelbar zu beheben. Falls das nicht geht, sind die Änderungen zurückzunehmen.
  • Teststabilisierung: Tests müssen stabil sein. Schlagen Tests sporadisch fehl, ist die Ursache zu finden und zu beheben. Die Lösungen sind dabei vielzählig und können unterschiedlich sein: der Einsatz von Test Doubles/Mocks statt echter Objekte, Entfernen von Abhängigkeiten zwischen den Tests, Umstrukturieren des zu testenden Codes.
  • qualitätsorientierte Entwicklungskultur: Wird der CI-Prozess im Entwicklungsteam nicht als zentrales qualitätssteigerndes Konzept verstanden, das hilft, Vertrauen in den Code aufzubauen, Frust zu verhindern und die Produktivität zu steigern, wird er nicht funktionieren. Dann lassen sich leicht bedeutungslose Tests für eine hohe Testabdeckung finden oder der CI-Server "mal eben" deaktivieren, wenn der Build fehlschlägt. Dem zu begegnen ist keine technische Frage, sondern eine Frage der Entwicklungskultur. Erst eine Atmosphäre, die die Konzentration auf die Aufgaben und Ziele ermöglicht, das Vertrauen fördert und die Übernahme von Verantwortung zulässt, bietet hierfür die Basis.