Dieser Beitrag erläutert, wie Continuous Integration für Webapps basierend auf Java und javaScript umgesetzt werden kann. Neben dem Prozess des Continuous Integration (CI) wird eine konkrete Lösung für die Umsetzung des Prozesses mit der Software Jenkins präsentiert. Die verteilte Architektur, zwei Programmiersprachen, viele Frameworks und verschiedene Browser als Zielsysteme erzeugen eine nicht unerhebliche Komplexität. Da die praktische Einrichtung von CI in diesem Setting nicht trivial ist und ein Überblick zu diesem Thema mir im Web bisher fehlte habe ich mich entschlossen, meine Konfiguration und das Zusammenspiel der beteiligten Software hier zu erläutern.
Grundlagen des Continuous Integration
Je komplexer Software wird, desto mehr gewinnen Punkte wie die Abhängigkeiten zwischen ihren Bestandteilen und auch die Umgebung auf der eine Software getestet wird, an Bedeutung. Continuous Integration (kurz CI) kann (auch kleinen) Teams dabei helfen, den Überblick über den Entwicklungsstand der eigenen Software zu behalten. Im einfachsten Fall werden einzelne Module der Software nach Änderungen kompiliert und stehen bei Erfolg auf dem CI-Server bereit. Darauf aufbauend können Abhängigkeiten zwischen den vorhandenen Modulen aufgelöst und ähnlich einem an den Kunden ausgelieferten Release eine ausführbare Version erstellt werden. Idealerweise ist dabei neben der Kompilierbarkeit der Software auch sichergestellt, dass die Software den an sie gestellten Anforderungen entspricht und somit „das richtige tut“.
Dies lässt sich dadurch überprüfen, dass zunächst Tests für die von der Software bereitgestellten Funktionalität geschrieben wurden und diese im Rahmen des CI-Prozesses im Anschluss an die Kompilierung der Software ausgeführt werden. Bei den Tests sind Unit-Tests und End-to-End-Tests zu unterscheiden. Während erstgenannte möglichst atomare Bestandteile der Softwarefunktionalität testen und sich etwa im Rahmen eines testgetriebenen Entwicklungsmodells als Spezifikation für die zu entwickelnde Software verwenden lassen. End-to-End-Tests hingegen sind auf die Überprüfung der Gesamtapplikation unter Nutzung der Benutzeroberfläche (UI) ausgerichtet.
Sobald in einem der genannten Schritte ein Fehler auftritt, sollte der für die Änderung verantwortliche Entwickler sowie der/die Verantwortliche(n) der betroffenen Softwareteile bzw. des Gesamtprodukts darüber in Kenntnis gesetzt werden, um Fehler zu korrigieren. Idealerweise wissen somit die Mitglieder des Teams jederzeit darüber Bescheid, ob die von Ihnen verantwortete Funktionalität korrekt umgesetzt wurde und ein lauffähiger Zustand des Gesamtprodukts gegeben ist.
Zu einer Automatisierung dieser Aufgaben zu gelangen, ist je nach eingesetztem Technologiestack des eigenen Produkts nicht unbedingt trivial. In diesem Beitrag möchte ich daher einen Überblick geben, wie eine CI-Lösung für Web-Applikationen auf Basis von Java 8 auf Serverseite und HTML5 / Javascript auf Clientseite aussehen kann. Der Artikel gibt eine Übersicht über eine funktionsfähige CI-Lösung auf Basis nicht-proprietärer Komponenten.
Softwarestack für die Kombination Java / JavaScript
CI ist ein etabliertes Prinzip und wird entsprechend von diversen Softwareprodukten unterstützt. Spannend wird es im Rahmen der Produktauswahl, wenn mehr als eine Basistechnologie eingesetzt wird, wie die hier besprochene Kombination von in Java geschriebener Serverlogik und ein Javascript Rich Client. Die Automatisierung des Builds für dieses Setting erfolgt in diesem Artikel unter Verwendung des CI-Servers Jenkins, des Buildmanagement-Tools Maven und den node.js Paketen karma, protractor und grunt. Haben Sie also Bedarf an einer CI-Lösung, so können sie die hier vorgestellte aufgrund der ausgewählten Komponenten direkt selbst aufbauen und konfigurieren.
Um Ihnen die einzelnen Bestandteile des CI-Softwarestacks näherzubringen, möchte ich sie Ihnen im Folgenden kurz vorstellen. Das Versions- und Änderungsmanagement erfolgt im Rahmen des Artikels mittels eines git-repositories. Java-Quellcode wird mit JUnit, Javascript mit in jasmine-Syntax verfassten Unittests über den Testrunner karma getestet. Das node.js Paket protractor dient schließlich der Ausführung der End-to-End-Tests. Bevor wir die Kombination dieser Bestandteile innerhalb des CI-Prozesses betrachten, hier eine Übersicht mit den Bezugsquellen der Softwarekomponenten.
CI-Server
jenkins: Bei jenkins handelt es sich um einen nicht-proprietären CI-Server, welcher über Plugins erweitert werden kann.
- git-plugin : Dieses Jenkins-Plugin ermöglicht den Zugriff auf git-repositories. Nutzen Sie ein anderes SCM-System sind weitere Plugins für jenkins verfügbar.
- Xvfb-plugin: Ein Jenkins-Plugin, um einen virtuellen Framebuffer vor einem Bauvorgang zu starten und nach dessen Abschluss zu beenden. Dies ermöglicht die Ausführung von End-to-End-Test mit „echten“ Browsern auch auf dem CI-Server ohne Anzeigegeräte.
Java-Build, Unit-Tests und Prozesssteuerung
Maven 3.3 wird zur Verwaltung von Abhängigkeiten zwischen unseren Softwaremodulen und externen Programmbibliotheken sowie für die übergeordnete Steuerung des Bauvorganges verwendet. Dabei werden folgende Plugins eingesetzt:
- frontend-maven-plugin : Da Maven den Build-Prozess koordiniert, ist dieses Plugin erforderlich, um node.js Pakete für den Test von Javascript und die Ausführung der End-to-End-Tests installieren und verwenden zu können.
- maven-dependency-plugin : Dieses Plugin wird genutzt, um die war-Files der Applikationsmodule in den Kontextpfad des Tomcat zu kopieren.
- tomcat7-maven-plugin: Dieses ermöglicht die Ausführung eines Tomcat 7 im Rahmen eines Maven-Goals. Somit ist es möglich, die Client und Server-Module der zu testenden Applikation auf einer Testumgebung zu deployen.
JUnit übernimmt schließlich die Ausführung von Unit-Tests für die serverseitige Java-Programmlogik.
Javascript Build und Unit-Tests
node.js stellt eine Javascript runtime dar, welche diverse Pakete für die Durchführung von Unit-Tests sowie End-to-End-Tests bereitstellt. Während des CI-Prozesses wird eine node.js Umgebung eingerichtet, welche den Download der benötigten Pakete erlaubt:
- karma ist ein sogenannter Testrunner zur Ausführung der in jasmine verfassten Testscripte für Javascript-Quellcode.
- karma-jasmine ist ein Adapter für das jasmine Testframework, welches die Programmierschnittstelle für die Spezifikation der Tests bereitstellt.
- karma-firefox-launcher: Dieses Paket ermöglicht die Ausführung der Unit-Tests in Firefox. Da die Interpretation von Javascript in verschiedenen Browsers abweichen kann, empfiehlt sich die Möglichkeit, unterschiedliche Konfigurationen testen zu können.
- karma-chrome-launcher dient der Ausführung von Unit-Tests in Chrome.
- karma-phantomjs-launcher ermöglicht die Ausführung der Unit-Tests in phantomjs, einem headless webkit-basierten Browser.
- karma-junit-reporter wird für den Export von Testergebnissen in ein der JUnit-Spezifikation entsprechendes xml-Format verwendet.
javaScript End-to-End-Tests
- protractor: Ein Framework, welches die Konfiguration und Ausführung von End-to-End-Tests unterstützt.
- selenium : Dieses Paket schafft eine Schnittstelle zu den gewünschten Browsern, um interaktionsbasierte Testspezifikationen automatisiert in realen Browsern auszuführen (siehe auch http://www.seleniumhq.org/)
- selenium-webdriver repräsentiert die Implementierung der selenium Schnittstelle für den Browser Firefox.
- chromedriver : Dieses Paket stellt eine selenium-webdriver Implementierung für den Chrome-Browser dar. Weitere unterstützte Browser sind hier gelistet.
- jasmine-reporters: Ermöglicht die Generierung von JUnit-Reports für die Ergebnisse von Jasmine-Tests
- grunt : javascript-basierter task runner, welcher für Operationen auf dem Dateisystem und die Ausführung der End-to-End-Tests mittels protractor verwendet wird.
- grunt-protractor-runner: Grunt-plugin, welches protractor startet.
- grunt-shell : Grunt-plugin, welches shell-Kommandos ermöglicht.
Ablauf des CI-Prozesses
Für den Aufbau der CI-Umgebung ist also eine nicht unerhebliche Zahl an Softwarekomponenten erforderlich. Im nächsten Schritt sind diese sinnvoll miteinander zu verknüpfen, um den CI-Prozess von der Feststellung einer Softwareänderung, über die Schritte Build und Test bis hin zum Reporting zu realisieren. Die folgende Abbildung gibt einen ersten Überblick über das Zusammenwirken innerhalb des CI-Softwarestacks. Die einzelnen Schritte werden anschließend kurz erläutert.
- Nachdem ein Entwickler eine Änderung vorgenommen hat, erfolgt zunächst ein „git push“ von mindestens einem commit von dem Entwicklerrechner auf das git remote.
- Innerhalb des git repositories ist ein sogenannter „post-receive“ hook einzurichten, welcher die Benachrichtigung des CI-Servers (jenkins) hinsichtlich der Änderung veranlasst.
- Nachdem jenkins über die Änderung informiert wurde, aktualisiert dieser seine Arbeitskopie und veranlasst den Build für die einzelnen Module der Webapp
- Der Build der Server- und Client-Module erfolgt über Maven (Maven-Build, JUnit-Tests, Javascript Unit-Tests)
- (Server) Im Fall der Java-basierten Servermodule erfolgt die Ausführung der JUnit-Tests in Zusammenhang mit dem Build.
- (Client) Ist das aktuell betrachtete Modul ein Javascript-basiertes Client-Modul wird mittels des frontend-maven-plugin eine Node.js Installation veranlasst.
- (Client) Anschließend können die in der in der „package.js“ Datei des aktuellen Moduls benannten node packages per frontend-maven-plugin installiert werden. Dies sind entsprechend des vorangegangenen Abschnitts folgende:
- karma,
- karma-jasmine,
- karma-firefox-launcher,
- karma-chrome-launcher,
- karma-phantomjs-launcher,
- karma-junit-reporter
- (Client) Liegen die Pakete des karma-Testrunner vor, kann seine Ausführung für die Unit-Tests per frontend-maven-plugin gestartet werden.
- Bei einem Fehler oder einem fehlschlagendem Test erfolgt bei jedem dieser genannten Schritte ein Abbruch des Builds. Die Testergebnisse werden in Form der JUnit-Testresult-XML-Datei generiert und in das Verzeichnis /target/surefire-reports/ geschrieben. Lagen keine Fehler oder fehlgeschlagenen Tests vor, wird mit dem nächsten Schritt fortgefahren.
- Das Deployment per Maven richtet nun eine Umgebung zur Ausführung der Anwendung ein
- Hierzu installieren wir zunächst Node.js . Die separate Installation erlaubt bei Bedarf eine zu den Modulen des Client abweichende Version von node.js.
- Ähnlich zum Build der Module werden nun die in der „package.js“ definierten node.js packages per frontend-maven-plugin installiert. Innerhalb dieses Beispiels also:
- grunt,
- grunt-cli,
- protractor,
- chromedriver,
- grunt-protractor-runner,
- grunt-shell,
- jasmine-reporters,
- selenium
- Ein Update des webdriver managers erfolgt per grunt-task „webdriverUpdate“(definiert in Gruntfile.js, gestartet über frontend-maven-plugin). Dies installiert den für die Ausführung der end-to-end-tests notwendigen webdriver.
- Nun können die Modul warfiles in das Verzeichnis „war“ innerhalb des Buildverzeichnisses per maven-dependency-plugin deployed werden.
- Der Start einer Tomcat7-Instanz erfolgt unter Nutzung des im vorherigen Schritt definierten „war“ Verzeichnisses mittels des tomcat7-maven-plugin.
- Wenn der Start des Tomcat erfolgreich war, können die End-to-End-Tests durchgeführt werden.
- Der Start des protractor testrunners zur Ausführung der End-to-End-Tests erfolgt per grunt-task „test-e2e“ (gestartet über frontend-maven-plugin).
- Die Tomcat7-Instanz wird mittels tomcat7-maven-plugin heruntergefahren.
- Bei einem Fehler oder einem fehlschlagendem Test erfolgt der Abbruch des Builds. Ansonsten wird die JUnit Testresult xml in das Verzeichnis /target/surefire-reports/ geschrieben.
Zusammenfassung
Mit den genannten Komponenten und Ihrer Kombination innerhalb des auf Jenkins und Maven basierenden CI-Prozesses lassen sich Projekte, welche sowohl Java als auch Javascript verwenden vollumfänglich automatisiert testen und deployen. Die detaillierte Konfiguration der einzelnen Komponenten werde ich in zukünftigen Artikeln behandeln.