Testmodule
Maven-Module verwalten Produktiv- und Testcode in getrennten Verzeichnissen (src/main und test). Das Packaging (z.B. als JAR) exkludiert Testklassen und -ressourcen. Damit andere Module Test-Hilfsklassen, Mock-Implementierungen, etc. verwenden können, müssen diese in src/main liegen. Elemente von src/test bleiben für andere Module verborgen. Um Testklassen wiederzuverwenden, lagert man in diese in ein separates Testmodul aus. Das sieht z.B. wie folgt aus:
Der Testcode und die Hilfsklassen für “framework” sind nach “framework-test” ausgelagert worden. Die Test-Hilfsklassen operieren auf “framework”-Klassen. Die Testklassen benötigen die Testhilfsklassen. Würden die Tests innerhalb von “framework” liegen gebe es einen Modulzyklus. Dies ist weder sinnvoll noch von Maven erlaubt.
Wer seine Testabdeckung automatisiert ermittelt bekommt mit dieser Modulaufteilung Probleme. Per Default ermitteln Cobertura, Clover, Emma die Unit-Test-Abdeckung nur pro Modul. Obwohl umfangreiche Tests für den Code in “framework” existieren, liefert eine Messung 0% Abdeckung.
Lösung mit JaCoCo
JaCoCo wird von den Emma-Machern entwickelt und arbeitet über Byte-Code-Instrumentation zur Laufzeit mittels eines JVM-Agents.
Mittels JaCoCo-Agent und dem JaCoCo-Sonar-Plugin lässt sich das Testabdeckungsproblem lösen und sogar die Abdeckung für Integrationstests messen. Das im Folgenden beschriebene Setup umfasst Jenkins / Hudson sowie Sonar Konfiguration und Anpassungen an Euren Maven POMs.
1. JaCoCo bereitstellen
Das “jacoco-agent.jar” wird auf dem System, welches die Tests ausführt (z.B. Euer CI Server), benötigt.
- ZIP herunterladen (wget …) z.B. nach /var/lib/jacoco. Achte darauf, dass Deine Sonar-Installation zum JaCoCo-Agent kompatibel ist oder entschließe Dich, Deine Sonar-Installation upzugraden.
- unzip
2. POM anpassen
Das Surefire Plugin muss den JaCoCo-Agent einbinden, wenn es die Tests ausführt. Der Agent muss Coverage-Informationen für alle Eure Module in dieselbe Datei schreiben.
1: <profiles>
2: <profile>
3: <id>ci</id>
4: <build>
5: <pluginManagement>
6: <plugins>
7: <plugin>
8: <artifactId>maven-surefire-plugin</artifactId>
9: <configuration>
10: <argLine>-javaagent:${jacoco.agent.path}=destfile=${jacoco.unit.path}</argLine>
11: </configuration>
12: </plugin>
13: </plugins>
14: </pluginManagement>
15: </build>
16: </profile>
17: </profiles>
3. Jenkins / Hudson Job konfigurieren
Jenkins muss die Variablen “jacoco.agent.path” und “jacoco.unit.path”. Dazu bei “Goals und Optionen” in der Job-Konfiguration z.Bsp. folgendes angeben:
clean install -P ci -Djacoco.agent.path="/var/lib/jacoco/lib/jacocoagent.jar" -Djacoco.unit.path="/tmp/mapgame-jacoco-unit"
Gib bei der Sonarkonfiguration des CI-Jobs die Option “-Dsonar.jacoco.itReportPath=/tmp/mapgame-jacoco-it“ an. Das JaCoCo-Sonar-Plugin erkennt diese System-Property und findet dadurch die Report-Datei.

4. Sonar JaCoCo Plugin installieren
Installiere im Sonar Update Center das Plugin “JaCoCo”. Konfiguriere unter Sonar – Settings – Core – Code Coverage Plugin den Wert “jacoco”. Hast Du alles korrekt gemacht, so wirst Du auf Deinem Sonar-Dashboard nach Lauf des CI-Jobs im Jenkins / Hudson den korrekten Wert für die Unit-Testabdeckung sehen.

Trennung Unit- und Integration-Tests
JaCoCo kann natürlich auf die Integration-Test-Abdeckung messen. Dies solltest Du getrennt von der Unit-Test-Messung halten. Mit Unit-Tests kann nach TDD entwickelt werden, mittels Integration-Tests nicht wirklich, welche Change & Test-Phase zu lang dauert und Unit-Tests eine viel lokalere Betrachtung erlauben. Dadurch ist Bugfixing bei Testfehlschlägen effizienter.
Für Integrationstests sollte das “maven-failsafe-plugin” verwendet werden, da es Aufräumarbeiten nach Integrationstests (Server stoppen, etc.) besser beherrscht (post-integration-test Phase wird bei Test-Fehlschlägen aufgerufen. Dies ist bei Surefire nicht der Fall.).
1. POM erweitern
Die Code-Abdeckung in den Integration-Tests selbst soll gemessen werden:
<plugin> <artifactId>maven-failsafe-plugin</artifactId> <configuration> <argLine>-javaagent:${jacoco.agent.path}=destfile=${jacoco.it.path}</argLine> </configuration></plugin>
Natürlich musst Du den JaCoCo-Agent auch in den getesteten Systemen einbinden. Bei mir ist dies die lokale Google App Engine.
<profiles> <profile> <id>ci</id> <build> <pluginManagement> <plugins> <plugin> <groupId>net.kindleit</groupId> <artifactId>maven-gae-plugin</artifactId> <executions> <execution> <id>start-app-engine</id> <phase>pre-integration-test</phase> <goals> <goal>start</goal> </goals> <configuration> <javaAgent>${jacoco.agent.path}=destfile=${jacoco.it.path}</javaAgent> </configuration> </execution> </executions> </plugin> </plugins> </pluginManagement> </build> </profile></profiles>
2. Jenkins / Hudson CI Job erweitern
- Zusätzlicher Parameter bei “Goals und Optionen”: -Djacoco.it.path="/tmp/mapgame-jacoco-it"
- Zusätzlicher Parameter bei Sonar MAVEN_OPTS: -Dsonar.jacoco.itReportPath=/tmp/mapgame-jacoco-it
3. Sonar Dashboard erweitern
- Im Sonar Dashboard “configure widgets” klicken
- “IT coverage widget” hinzufügen
Fazit
JaCoCo funktioniert!
Hohe Code-Abdeckung durch gut strukturierte Unit- und Integrationstests, die jeweils nur eine “Sache” testen, sind die halbe Miete für gute Softwarequalität!