Software-Qualität mit JaCoCo und SonarQube

JaCoCo ist eine Open-Source-Bibliothek für Java, die die Code-Abdeckung (Code Coverage) einer Applikation misst. Die Code-Abdeckung ist ein wichtiges Kriterium für die Beurteilung der Testabdeckung einer Applikation, denn durch geeignete Tests und eine hohe Testabdeckungsrate wird der Produktionscode gründlich überprüft.

Einige Features von JaCoCo sind beispielsweise die Erstellung von Reports in verschiedenen Formaten (HTML, XML, CSV) und die Integration in IDEs und CI – Pipelines. In Kombination mit SonarQube oder Jenkins kann somit die Code Coverage außerhalb der IDE stattfinden und durchs Reporting für das Entwicklungsteam zentral und transparent verfügbar gemacht werden.

In diesem Artikel zeige ich die Einrichtung von JaCoCo mit JUnit Tests, sowie die Integration der Ergebnisse in SonarQube.

Testapplikation

In den folgenden Beispielen werden wir ein simples Calculator-Programm verwenden, das eine ganzzahlige Division durchführt. Wichtig zu wissen: JaCoCo benötigt nicht den tatsächlichen Quellcode einer Applikation, um Reports über die Coverage Rate der Tests zu erstellen!

public final class Calculator {
    private Calculator() {}

    public static int divide(int a, int b) {
        int result = a / b;
        boolean resultWasPositive = false;
        if (result < 0)
            System.out.println("Negative result: " + result);
        else {
            System.out.println("Positive result: " + result);
        }
        return result;
    }
}

Das Ergebnis der Division wird in der Konsole ausgegeben und schließlich an den Aufrufer zurückgegeben. Die Fehler dieses Codes sind eindeutig erkennbar. Wie hilft uns nun JaCoCo, bessere Tests zu schreiben?

JaCoCo-Konfiguration

In der pom.xml-Datei des Maven-Projekts, fügen wir JaCoCo als dependency ein:

<dependencies>
        ....
        <dependency>
            <groupId>org.jacoco</groupId>
            <artifactId>jacoco-maven-plugin</artifactId>
            <version>0.8.8</version>
        </dependency>
      ....
</dependencies>

Für einfache Unit-Tests, reichen 2 Goals für das Plugin (weitere Goals hier).

<build>
        <plugins>
            <plugin>
                <groupId>org.jacoco</groupId>
                <artifactId>jacoco-maven-plugin</artifactId>
                <version>0.8.8</version>
                <executions>
                    <execution>
                        <id>default-prepare-agent</id>
                        <goals>
                            <goal>prepare-agent</goal>
                        </goals>
                        <configuration>
                            <destFile>
                                ${project.build.directory}/coverage-reports/jacoco.exec
                            </destFile>
                        </configuration>
                    </execution>

                    <execution>
                        <id>default-report</id>
                        <phase>test</phase>
                        <goals>
                            <goal>report</goal>
                        </goals>
                        <configuration>
                            <dataFile>${project.build.directory}/coverage-reports/jacoco.exec</dataFile>
                            <outputDirectory>${project.reporting.outputDirectory}/jacoco</outputDirectory>
                        </configuration>
                    </execution>

                </executions>
            </plugin>
        </plugins>
    </build>
  • prepare-agent: Bereitet die JaCoCo-Runtime vor, registriert die Anzahl der von Tests ausgeführten Zeilen usw. Zusätzlich kann hier über das <configuration>-Tag angegeben werden, wo und unter welchem Namen der erstellte Report (im Binärformat) gespeichert werden soll (standardmäßig jacoco.exec).
  • report: Erstellt die lesbaren Reports (XML, HTML oder CSV) für Code Coverage in der Testphase. Zusätzlich kann hier angegeben werden, wo die Reports gespeichert werden sollen und wo die jacoco.exec-Datei zu finden ist, falls diese in dem prepare-agent Goal konfiguriert wurde.

Testfälle schreiben

Nun wollen wir einen einfachen Testfall schreiben, der überprüfen soll, ob die ganzzahlige Division von 10 und 4 das Ergebnis 2 zurückliefert und messen anschließend seine Testabdeckung.

public class CalculatorTest {

    @Test
    public void testDivision() {
        int a = 10;
        int b = 4;
        int expected = 2;
        int actual = Calculator.divide(a, b);
        assertEquals(expected, actual);
    }
}

Die Report-Datei über die Code-Abdeckung unserer Tests finden wir  nach der Ausführung von mvn test standardmäßig unter /target/site/jacoco/index.html.

Was sehen wir hier? Unser Testfall mit den Eingaben 10 und 4 hat eine Code-Abdeckung von 64% erreicht. 11 Anweisungen wurden nicht erreicht und nur eine von 2 Verzweigungen (da nur ein if-else-Konstrukt im ursprünglichen Programmcode) wurde ausgeführt. Wir haben also das Programm unzureichend getestet und den Testfall mit negativen Zahlen vergessen. Das können wir genauer sehen, wenn wir auf das Element divide(int, int) im Report klicken.

Mit der folgenden Farbkodierung:

  • Grün: wurde völlig ausgewertet während des Testens
  • Gelb: nur partielle Auswertung
  • Rot: keine Ausführung durch die Tests

Wir müssen also einen weiteren Testfall schreiben, der auch die Verzweigung im if-Fall ausführt.

@Test
    public void testDivision_positive() {
        int a = 10;
        int b = 4;
        int expected = 2;
        int actual = Calculator.divide(a, b);
        assertEquals(expected, actual);
    }
    @Test
    public void testDivision_negative() {
        int a = 10;
        int b = -5;
        int expected = -2;
        int actual = Calculator.divide(a, b);
        assertEquals(expected, actual);
    }

Die erweietrten Tests erreichen nun eine vollständige Anweisungs- und Verzweigungsüberdeckung. Nichtsdestotrotz ist die Code Abdeckung keine Grundlage für die Qualität der Tests oder der Software! In der divide-Methode der Calculator-Klasse wird z.B. überhaupt nicht auf Division durch 0 geachtet!

Mit JaCoCo lässt sich auf diese Weise der Umfang der dynamischen Qualitätssicherung durch die aktive Testdurchführung messen und transparent beleuchten. Doch etliche Probleme mit der Code-Qualität lassen sich auch bereits vor der Testdurchführung durch die statische Code-Analyse identifizieren. Und genau hier kommt ein weiteres Werkzeug ins Spiel: SonarQube.

Integration mit SonarQube und Jenkins

Mit SonarQube lässt sich die statische Qualität des Codes messen und überprüfen. Die Funktionsweise von SonarQube ist folgendermaßen (übernommen von der offiziellen Dokumentation):

SonarQube Instance Components
  1. Entwickler schreiben Code und commiten diesen zu einem Repository.
  2. Eine CI-Pipeline durchläuft automatisch die verschiedenen Stages (Build, Tests…). Ein integrierter SonarQube Scanner analysiert die Ergebnisse.
  3. Die Analyse vom Scanner wird auf einem SonarQube Server hochgeladen und die Entwickler bekommen nun Feedback im Dashboard, als E-Mail oder direkte Benachrichtigung in IDE durch sonarlint.

SonarQube Scanner Plugin für Jenkins ermöglicht eine zentrale Konfiguration des SonarQube-Servers in Jenkins. Somit lässt sich in der CI-Pipeline eine Analyse mit einem SonarScanner (z.B. SonarScanner für Maven) starten, die nach ihrer Ausführung auf der Jenkins Job Seite zum SonarQube Dashboard verlinkt (wie im Schritt 3. der Funktionsweise erläutert). Sehen wir uns das Dashboard an.

Hier ist eine Zusammenfassung der SonarQube-Analyse zu finden mit diesen Bezeichnungen:

  • Quality Gate Status: Hier geht es letztendlich nur um die Frage „Ist mein Projekt bereit für ein Release?„. Um diese Frage zu beantworten, können wir Mindestanforderungen stellen wie z.B. eine Code Coverage Rate von mind. 80%. Die Quality Gates sind idealerweise für alle Projekte gleich, lassen sich aber einzeln einstellen, da wir ja per Projekt unterschiedliche Anforderungen haben können (z.B. verschiedene Coverage Rates für Java oder Web-Applikationen). Außerdem kann die Jenkins Pipeline unterbrochen werden, wenn der Quality Gate Status fehlgeschlagen ist.
  • Bugs, Vulnerabilities, Security Hotspots: keine für den verwendeten Code oder die 2 Unit Tests
  • Code Smells: im Zusammenhang mit der Wartbarkeit des Codes. Je mehr Code Smells, desto schwieriger ist es, den Code zu warten. Änderungen am Code können sogar zu zusätzlichen Fehlern in der Software führen
  • Debt: Die geschätzte Zeit, die benötigt wird, um alle Wartbarkeitsprobleme / Code Smells zu beheben

In der Calculator-Klasse aus dem o.s. Beispiel werden durch SonarQube 4 Code Smells identifieziert:

  • Wir haben für die Klasse kein package definiert,
  • deklarieren unnötigerweise die boolean-Variable resultWasPositive, ohne sie wieder zu verwenden,
  • und uns wird vorgeschlagen, statt System.out ein Logger zu benutzen.

Man kann für zusätzliche Informationen auf Why is this an issue? klicken, um mehr über das zugrundeliegende Problem zu erfahren.

Die restlichen 3 Code Smells sind in der Testklasse, die sich aber alle auf den Modifier public der CalculatorTest-Klasse und der Testmethoden beziehen.

In diesem Artikel haben wir einfache Möglichkeiten für die Einrichtung und Nutzung von JaCoCo und SonarQube beschrieben. Probiere es am besten gleich in Deinen Projekten aus – die investierte Zeit zahlt sich i.d.R. durch die schnellere Fehlerfindung und Analysemöglichkeiten sehr schnell wieder aus.

 

0 Kommentare

Hinterlasse einen Kommentar

An der Diskussion beteiligen?
Hinterlasse uns deinen Kommentar!

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert