Das Testframework JaCoCo einbinden: Metriken der Codeabdeckung
https://bildung.social/@oerinformatik/
https://oer-informatik.de/sbb07_jacoco_codeabdeckung
tl/dr; (ca. 11 min Lesezeit): Das Maven-Projekt wird so konfiguriert, dass nach jedem Unit-Test-Durchlauf die Überdeckungstestmetriken gespeichert und als Report aufbereitet werden. Dieser Artikel ist ein Teil der Artikelserie zu einem Adressbuch-SpringBoot-Projekt. Weiter geht es zu gegebenem Zeitpunkt. (Zuletzt geändert am 04.09.2023)
Wir wollen die Möglichkeiten des Testframeworks jUnit erweitern um Metriken, die die Güte der Testabdeckung messen. Welche Programmzeilen werden von den bestehenden jUnit-Tests überhaupt erreicht? Werden alle Bedingungen überprüft? Diese und andere Frage können wir mit Codeabdeckungsmetriken (code coverage) beantworten.
JaCoCo heißt das Framework, dass wir für Java-Projekte nutzen können (das steht für JavaCodeCoverage).1
Wir nutzen die Vorteile der modularen Softwareentwicklung und des Build-Tools Maven, um diese Metriken in unsere bestehende Tests und den normalen Arbeisablauf einbinden zu können. Hierzu ist zunächst einiges an Konfigurationsarbeit nötig. Die meisten Ideen hier sind dem Tutorial von Petri Kainulainen entnommen 2.
Anpassungen in der pom.xml
Um die Testabdeckung automatisch für Unit-Tests zu messen müssen wir:
- bereits eingebundene Maven-Plugins konfigurieren
- neue Plugins einbinden
- Maven Profile vorbereiten, damit wir zukünftig zwischen schnell laufenden Unit-Tests und langwierigeren Integrationstests unterscheiden können.
Anpassungen unter <project><build><plugins>
Folgende Ergänzungen zur bestehenden Konfiguration in der pom.xml
sind erforderlich:
<plugin>
-Abschnitt für spring-boot-maven-plugin
anpassen
Der <plugin>
-Abschnitt der artifactId
von spring-boot-maven-plugin
muss wie folgt lauten (Kann zwischen dem betreffenden <plugin>
und </plugin>
ersetzt werden):
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<jvmArguments>${argLine}</jvmArguments>
</configuration>
<executions>
<execution>
<id>start-spring-boot</id>
<phase>pre-integration-test</phase>
<goals>
<goal>start</goal>
</goals>
</execution>
<execution>
<id>stop-spring-boot</id>
<phase>post-integration-test</phase>
<goals>
<goal>stop</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
-Abschnitt für maven-surefire-plugin
einfügen
Das Surefire-Plugin ist für das Ausführen der Unittests in der Maven-test
-Phase verantwortlich. Die Einstellungen hier sorgen dafür, dass entweder Unittests oder Integrationstests (dazu später mehr) ausgeführt werden:
- Es werden keine Unit-Tests ausgeführt, wenn die
skip.unit.test
Flag gesetzt ist. Diese Flag nutzen wir später, wenn wir nur Integrationstests ausführen wollen (<skipTests>...
). - Es werden in dieser Phase keine Integrationstests ausgeführt, sofern man sich an die Konvention hält, dass deren Namen dem Muster
IT*.java
genügen (<excludes>...
).
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.0.0-M7</version>
<configuration>
<!-- Sets the VM argument line used when unit tests are run. -->
<argLine>${surefireArgLine}</argLine>
<!-- Skips unit tests if the value of skip.unit.tests property is true -->
<skipTests>${skip.unit.tests}</skipTests>
<!-- Excludes integration tests when unit tests are run. -->
<excludes>
<exclude>**/IT*.java</exclude>
</excludes>
</configuration>
</plugin>
<plugin>
-Abschnitt für jacoco-maven-plugin
einfügen
Als nächsten müssen wir das eigentliche Code-Coverage-Plugin Jacoco einbinden. Eigentlich wäre das mit den ersten drei Tags erledigt (<groupId><artifactId><version>
). Wir wollen aber zwei Dinge erreichen:
Die Code-Coverage-Reports sollen automatisch erstellt werden, nachdem die Tests durchgelaufen sind. Es muss also ein Pfad festgelegt werden, an dem die Daten der Tests gespeichert werden (
<destfile>...
bzw. beim Auslesen später:<datafile>...
). Zudem muss ein Ort festgelegt werden, an dem der Report dann gespeichert werden soll (<outputDirectory>...
). Wir nutzen jeweils Maven-Variablen für die Pfade.Die Ausgabe der Unit-Tests soll später von denen der Integrationstests getrennt werden. Daher fügen wir hier allen Unittest-Reports ein “-ut”-Suffix an (Beispiel:
jacoco-ut.exec
)
Im Ganzen sieht dieser Teil dann so aus:
<plugin>
<groupId>org.jacoco</groupId>
<artifactId>jacoco-maven-plugin</artifactId>
<version>0.8.8</version>
<executions>
<execution>
<id>pre-unit-test</id>
<goals>
<goal>prepare-agent</goal>
</goals>
<configuration>
<!-- Sets the path to the file which contains the execution data. -->
<destFile>${project.build.directory}/coverage-reports/jacoco-ut.exec</destFile>
<!--Sets the name of the property containing the settings for JaCoCo runtime agent.-->
<propertyName>surefireArgLine</propertyName>
</configuration>
</execution>
<!-- Ensures that the code coverage report for unit tests
is created after unit tests have been run. -->
<execution>
<id>post-unit-test</id>
<phase>test</phase>
<goals>
<goal>report</goal>
</goals>
<configuration>
<!-- Sets the path to the file which contains the execution data. -->
<dataFile>${project.build.directory}/coverage-reports/jacoco-ut.exec</dataFile>
<!-- Sets the output directory for the code coverage report. -->
<outputDirectory>${project.reporting.outputDirectory}/jacoco-ut</outputDirectory>
</configuration>
</execution>
</executions>
</plugin>
<profiles>
-Abschnitt zur Unterscheidung von Integrationstests und Unittests einfügen
Ganz am Ende der pom.xml
, zwischen </build>
und </project>
, muss noch eine neuer Abschnitt ergänzt werden. Wir erstellen zwei Profil: einen für die Unit-Tests (<id>dev</id>
) und einen für die Intergrationstests (<id>integration-test</id>
).
Folgendes muss eingefügt werden:
<profiles>
<!-- The Configuration of the development profile -->
<profile>
<id>dev</id>
<activation>
<activeByDefault>true</activeByDefault>
</activation>
<properties>
<!--
Specifies the build.profile.id property that must be equal than the name of
the directory that contains the profile specific configuration file.
Because the name of the directory that contains the configuration file of the
development profile is dev, we must set the value of the build.profile.id
property to dev.
-->
<build.profile.id>dev</build.profile.id>
<!--
Only unit tests are run when the development profile is active
-->
<skip.integration.tests>true</skip.integration.tests>
<skip.unit.tests>false</skip.unit.tests>
</properties>
</profile>
<!-- The Configuration of the integration-test profile -->
<profile>
<id>integration-test</id>
<properties>
<!--
Specifies the build.profile.id property that must be equal than the name of
the directory that contains the profile specific configuration file.
Because the name of the directory that contains the configuration file of the
integration-test profile is integration-test, we must set the value of the
build.profile.id property to integration-test.
-->
<build.profile.id>integration-test</build.profile.id>
<!--
Only integration tests are run when the integration-test profile is active
-->
<skip.integration.tests>false</skip.integration.tests>
<skip.unit.tests>true</skip.unit.tests>
</properties>
</profile>
</profiles>
VSCode-Addons installieren
Um die Ergebnisse in VSCode direkt darstellen zu können gibt es zwei nützliche Addons:
Coverage Gutters zeigt direkt im Code an, welche Zeilen von den Tests erfasst werden und welche nicht.
LivePreview ermöglicht es, die Berichte in HTML direkt in VSCode anzusehen.

Damit sollte für’s erste das Handwerkszeug vorhanden sein. Starten wir einen ersten Versuch:
Erste Versuche: Testen und Testabdeckung ermitteln
Um die Abdeckung ermitteln zu lassen müssen wir die Tests auf bekanntem Weg ausführen, also z.B::
Im Projektexplorer im Abschnitt “Java Projekts” gibt es ein Play-Symbol neben dem Java-Filenamen des Tests
Wir rufen das entsprechende Maven-Goal auf: führe alles aus bis inklusive der Unittestphase mit allen Tests:
shell mvn test -f ".\pom.xml"
Im Menü unter: Anzeigen/Testen findet sich ein Play-Symbol für die Tests (und später auch das Testergebnis)

Wo finde ich die Coverage-Ergebnisse?
Die Ergebnisse der Testabdeckung werden im Projektverzeichnis als HTML-Bericht aufbereitet und unter /target/site/jacoco-ut
gespeichert. Diese Dateien lassen sich in jedem Browser öffnen.

Durch das installierte “Live Preview”-Addon in VSCode können wir aber direkt per Rechtsklick auf “index.html” und “Show Preview” den Bericht öffnen:

Wir erhalten zunächst eine Übersicht der obersten Ebene unseres Projekts mit klickbaren Links zur Navigation zu den einzelnen Packages:

Auf den Seiten der einzelnen Packages finden sich Übersichten zu den Klassen in diesen Packages versehen mit Links, um zu den Ergebnisseiten der Klassentests zu navigieren:

Auf der Seite einer Klasse schießlich findet sich die Übersich zu einzelnen Methoden, auch diese sind anklickbar:

Auf den Methdenseiten kann man Codezeilengenau sehen, welche Zeilen von den Tests erreicht wurden:

Interpretation der Abdeckungswerte
Wofür aber stehen die einzelnen Werte und welche Relevanz haben sie? Wir schauen uns die Ergebnisseite noch einmal Spalte für Spalte an:

Es wird dort ausgegeben:3
Missed Instructions Cov.: Anweisungsüberdeckungsgrad (C0): wie viel Prozent der insgesamt vohandenen Anweisungen werden durch die Tests ausgeführt?
Missed Branches Cov.: Zweigüberdeckungsgrad (C1): wie viele Prozent der Kanten von Verzweigungen des Kontrollflusses durch die Tests durchlaufen werden.
Missed Cxty: Zyklomatische Komplexität: Anzahl der linear unabhängigen Zweige in einem Kontrollfluß - eine Maßzahl zur Mindestanzahl an nötigen Testfällen. Unter “Missed” steht die Anzahl der nicht erreichten Verzweigungen)
Missed Lines: Zeilenüberdeckungsgrad: Anzahl der erreichten und nicht erreichten (Missed) Quellcodezeilen (in zwei Spalten)
Missed Methods : Methodenüberdeckung: Anzahl der erreichten und nicht erreichten (Missed) Methoden (in zwei Spalten)
Missed Classes: Klassenüberdeckung: Anzahl der erreichten und nicht erreichten (Missed) Klassen (in zwei Spalten, hier nicht zu sehen, weil bereits auf Klassenebene)
Überdeckungsgrade
Die Überdeckungsgrade geben an, wie viele Anweisungen, Zweige usw. von den Tests durchlaufen werden. Hintergründe zu den Überdeckungsmetriken finden sich in diesem Artikel.
Zyklomatische Komplexität
Die Zyklomatische Komplexität stellt ein Maß für die Wartbarkeit und Testbarkeit von Code dar. Die Zahl repräsentiert die Anzahl (linear) unabhängiger Pfade, entlang derer ein Codeblock durchschritten werden kann. Eine hohe Zyklomatische Komplexität bedeutet, dass zumeist auch viele Testfälle nötig sind und der Code schlecht erweiterbar und wartbar ist. Ab 10 gilt eine Methode als komplex, ab 20 ab sehr schwer zu testen. Hintergründe finden sich in diesem Artikel
Zeilenüberdeckungsgrad
Wieviele Quellcode-Zeilen wurden vom Test ausgeführt? Es sind nicht die Anweisungen, sondern die Programmzeilen relevant, z.B. bei if-Statements kommt es auf die Formatierung an (einzeilig oder mehrzeilig). Der Vorteil ist: Debugger liefert i.d.R. die Zahl der durchlaufenen Zeilen, daher einfach umzusetzen. Diese Kennziffer ist allerdings weniger aussagekräftig als der Anweisungsüberdeckungsgrad.
Methodenüberdeckung
Wie viele Methoden wurden von keinem Test aufgerufen. Diese Zahl ist unabhängig von der jeweiligen Anweisungs- und Zweigüberdeckung - und daher wenig aussagekräftig für die Testqualität. Trotzdem sollte geprüft werden, welche Methoden nicht getestet werden - und ggf. nachgebessert werden.
Klassenüberdeckung
Wie viele Klassen wurden von keinem der Tests genutzt. Analog zur Methodenüberdeckung kann diese Zahl eher dazu dienen, systematisch bislang ungetestete Codesequenzen zu finden. Als Maß der Codegüte dient sie nicht, da Klassen hier als “getestet” erscheinen, sobald der Test auch nur eine Codezeile darin ausführt.
Fazit und Nutzen
Auf Basis der so gewonnenen Erkenntnisse können weitere sinnvolle Testfälle ermittelt werden - oder toter Code entfernt werden.
Es gibt kein allgemeingültiges Mindestmaß an Codeabdeckung. Die jeweils erforderliche Testabdeckung sollte in Anbetracht des Anwendungsfalls nach einer Risikoabschätzung sowie der eigenen Fähigkeiten getroffen werden. Einen guten Rahmen gibt die Testivus -Geschichte “How much unit test coverage do you need” von Alberto Savoia wieder.
Nächste Schritte
Dieser Artikel ist ein Teil der Artikelserie zu einem Adressbuch-SpringBoot-Projekt. Weiter geht es zu gegebenem Zeitpunkt._
Hinweis zur Nachnutzung als Open Educational Resource (OER)
Dieser Artikel und seine Texte, Bilder, Grafiken, Code und sonstiger Inhalt sind - sofern nicht anders angegeben - lizenziert unter CC BY-SA 4.0. Nennung gemäß TULLU-Regel bitte wie folgt: “Das Testframework JaCoCo einbinden: Metriken der Codeabdeckung” von Hannes Stein, Lizenz: CC BY-SA 4.0. Der Artikel wurde unter https://oer-informatik.de/sbb07_jacoco_codeabdeckung veröffentlicht, die Quelltexte sind in weiterverarbeitbarer Form verfügbar im Repository unter https://gitlab.com/oer-informatik/java-springboot/Backend. Stand: 04.09.2023.
[Kommentare zum Artikel lesen, schreiben] / [Artikel teilen] / [gitlab-Issue zum Artikel schreiben]
Homepage von Jacoco / Eclemma: https://www.eclemma.org/jacoco/↩
Creating Code Coverage Reports for Unit and Integration Tests With the JaCoCo Maven Plugin↩
Genaue Angaben über die Spalten finden sich in der Dokumentation unter https://www.jacoco.org/jacoco/trunk/doc/counters.html↩