Mutation Testing mit Pitest – Teil 2: SonarQube

Dieser Post baut auf dem vorangegangenen Teil auf. Solltet ihr diesen noch nicht gelesen haben, empfehlen wir, euch einige Minuten Zeit zu nehmen und dies jetzt zu tun.

Im letzten Teil wurden die Grundlagen von Mutation Testing sowie die Konfiguration und Benutzung von Pitest als Mutation Testing Framework für JVM-basierte Sprachen erläutert. Es wurde erklärt, wie man Pitest in sein Projekt einbindet und mit Maven ausführt. Der Maven-Aufruf erfolgte allerdings per Hand, und die von Pitest generierten Reports wurden im target-Verzeichnis auf dem Entwicklungsrechner abgelegt. Möchte man allerdings Pitest beispielsweise automatisch jede Nacht ausführen und die generierten Reports mehreren Projektbeteiligten an zentraler Stelle zur Verfügung stellen, stößt man mit diesem Ansatz recht schnell – nämlich sofort – an seine Grenzen. Abhilfe schaffen kann da SonarQube, ein weit verbreitetes Tool zur statischen Codeanalyse, welches auch bei den Projekten der TRIOLOGY GmbH im Einsatz ist. In diesem Post erklären wir, welche Schritte nötig sind, um Pitest als Teil einer Jenkins-Pipeline auszuführen, und die Reports in SonarQube zu übertragen. An dieser Stelle wird davon ausgegangen dass sowohl Wissen über die Grundlagen von SonarQube als auch von Jenkins Pipelines bereits vorhanden sind. Sollte dies nicht der Fall sein, verweisen wir auf diesen Blogpost von Josha zum Thema Statische Codeanalyse mit SonarQube sowie die Dokumentation von Jenkins Pipeline. Als Tipp sei an dieser Stelle auch der Blogpost von Johannes über Codevervollständigung für Jenkinsfiles in IntelliJ IDEA erwähnt.

Setup

Es ist nicht Ziel dieses Posts die Einrichtung von Jenkins oder SonarQube zu erläutern, deshalb wird davon ausgegangen, dass sowohl ein Jenkins CI-Server als auch eine SonarQube-Instanz vorhanden und konfiguriert sind. Um ein derartiges Setup auf einfache Weise herstellen zu können, bietet sich beispielsweise die Verwendung von Cloudogu an.

Um Pitest-Reports in SonarQube zu integrieren, ist Jenkins nicht zwangsweise erforderlich, aus Gründen der Einfachheit und der Praxisnähe beschäftigt sich dieser Post allerdings mit dem Zusammenspiel von Jenkins, SonarQube und Pitest. Als Beispielprojekt kommt die Spring PetClinic zum Einsatz. Wir gehen davon aus, dass für das Projekt schon ein grundlegendes Jenkinsfile angelegt wurde, um es aus der Versionsverwaltung auszuchecken, zu kompilieren, Unit Tests auszuführen, die Code-Coverage zu messen und SonarQube-Reports hierfür zu generieren. Das Jenkinsfile hat folgenden Inhalt:

#!groovy
node {
    SONAR_URL = “sonar.qube”
    USERNAME = “username”
    PASSWORD = “password”

    stage('Checkout') {
        checkout scm
    }

    stage('Build') {
        mvn "clean install -DskipTests"
    }

    stage('Test') {
        String jacoco = "org.jacoco:jacoco-maven-plugin:0.7.9"
        mvn "${jacoco}:prepare-agent test ${jacoco}:report"
    }

    stage('SonarQube') {
        mvn "org.codehaus.mojo:sonar-maven-plugin:3.2:sonar -Dsonar.host.url=${SONAR_URL} " +
        "-Dsonar.login=${USERNAME} -Dsonar.password=${PASSWORD} -Dsonar.exclusions=target/** "
        
    }
}

void mvn(String args) {
    sh "./mvnw --batch-mode -V -U -e ${args}"
}

Die Variablen für die SonarQube-URL, den Usernamen und das Passwort müssen entsprechend dem jeweiligen Setup gesetzt werden. Allgemein sollte allerdings vermieden werden, Secrets wie zum Beispiel Passwörter in das Jenkinsfile zu schreiben. Abhilfe schaffen kann zum Beispiel das Credentials-Plugin für Jenkins.
Abgesehen von dem Jenkinsfile handelt es sich um die unveränderte PetClinic aus dem GitHub-Repository.

Ready. Set. Go!

Nachdem die Grundlagen gelegt sind, geht es nun an die eigentliche Arbeit.

Das Pitest-Plugin für Maven
Die Konfiguration des Pitest-Plugins für Maven unterscheidet sich nur geringfügig von der minimalen Konfiguration in der pom.xml, die am Ende des ersten Artikels stand:

     <plugin>
        <groupId>org.pitest</groupId>
        <artifactId>pitest-maven</artifactId>
        <version>1.2.4</version>
      </plugin>

Ohne weitere Konfiguration generiert das Plugin seine Reports lediglich im HTML-Format, mit dem Pitest-SonarQube-Plugin (dazu später mehr) nicht umgehen kann. Deshalb wird die bestehende Konfiguration um die Einstellung des Ausagebeformats auf XML erweitert:

     <plugin>
        <groupId>org.pitest</groupId>
        <artifactId>pitest-maven</artifactId>
        <version>1.2.4</version>
          <configuration>
              <outputFormats>
                  <outputFormat>XML</outputFormat>
              </outputFormats>
          </configuration>
      </plugin>

Mehr muss in der pom.xml nicht konfiguriert werden, es sei denn, man möchte einzelne Klassen beziehungsweise Tests von der Mutationsanalyse ausschließen. Mehr dazu findet sich in der Dokumentation des Plugins.

Das SonarQube Pitest-Plugin
Nachdem die Reports nun in einem maschinenlesbaren Format vorliegen, wird SonarQube so konfiguriert, dass es die Daten auch verwerten kann. Von Haus aus kann SonarQube mit den Mutation Test Reports nichts anfangen; glücklicherweise gibt es aber ein Plugin aus der Community, welches aus den XML-Reports SonarQube-Issues generiert.

Die Installation des Plugins erfolgt dabei am einfachsten über das Update Center der SonarQube-Instanz. Dieses findet man unter ‘Administration’ -> ‘System’ -> ‘Update Center’:

 
Nach dem Auswählen des Plugins und einem Klick auf ‘Install’ wird das Plugin heruntergeladen und installiert. Nach dem Abschluss der Installation muss SonarQube durch einen Klick auf den entsprechenden Button neu gestartet werden, um die Änderungen zu aktivieren.

Wichtig: SonarQube-Regel aktiveren
Es ist wichtig im Anschluss an die Installation, die SonarQube-Regel ‘Survived Mutant’ zu aktivieren. Vergisst man dies, werden keine Issues für überlebende Mutationen angelegt. Die Regel muss dafür dem verwendeten Quality Profile hinzugefügt werden. Das erreicht man, indem man im ‘Rules’-Reiter nach ‘Survived Mutant’ sucht und anschließend auf ‘Activate’ klickt. Nach der Auswahl des Quality Profiles (in unserem Beispiel der ‘Sonar Way’) ist die Regel aktiviert.

 
An dieser Stelle kann auch die ‘Rule Severity’ eingestellt werden. Die Standardeinstellung wertet nicht abgetötete Mutationen als ‘Major’ Issues.

Die SonarQube-Konfiguration ist hiermit abgeschlossen.

Falls gewünscht kann noch ein Pitest Widget auf dem Custom Dashboard abgelegt werden. Dies geschieht auf der Projektübersichtsseite nach einem Klick auf ‘Dashboards’ -> ‘Custom Dashboard’ -> ‘Configure Widgets’. Das Widget findet man dann unter dem Namen ‘Pitest Reports’.

Auf der Zielgeraden, oder: Jenkins
Theoretisch ist es jetzt schon möglich, die Pitest-Reports in SonarQube zu importieren. Exemplarisch sei das manuelle Vorgehen an dieser Stelle erwähnt:

 

  • Durchführen der Unit Tests

Bevor die Mutation Tests ausgeführt werden können, müssen erst einmal die Unit Tests durchgelaufen sein. Dies erreicht man über die Eingabe von mvn test.

  • Durchführen der Mutation Tests

Wie bereits im ersten Teil erwähnt, werden die Mutation Tests durch die Eingabe von org.pitest:pitest-maven:mutationCoverage ausgeführt.

  • Pushen der Reports an das Pitest-SonarQube-Plugin

Dieser Schritt ist neu. Durch das Aufrufen des SonarQube-Maven-Plugins mit dem Befehl mvn org.codehaus.mojo:sonar-maven-plugin:3.2:sonar -Dsonar.pitest.mode=reuseReport werden die von Pitest generierten Reports an SonarQube gepusht.

 

Natürlich möchte man dieses Vorgehen nicht regelmäßig manuell ausführen. Deshalb erweitern wir das Jenkinsfile entsprechend:

#!groovy
node {
    SONAR_URL = “sonar.qube”
    USERNAME = “username”
    PASSWORD = “password”

    stage('Checkout') {
        checkout scm
    }

    stage('Build') {
        mvn "clean install -DskipTests"
    }

    stage('Test') {
        String jacoco = "org.jacoco:jacoco-maven-plugin:0.7.9"
        mvn "${jacoco}:prepare-agent test ${jacoco}:report"
    }

    stage('Mutation Test') {
        mvn "org.pitest:pitest-maven:mutationCoverage"
    }

    stage('SonarQube') {
        mvn "org.codehaus.mojo:sonar-maven-plugin:3.2:sonar -Dsonar.host.url=${SONAR_URL} " +
        "-Dsonar.login=${USERNAME} -Dsonar.password=${PASSWORD} -Dsonar.exclusions=target/** " +
        "-Dsonar.pitest.mode=reuseReport"
    }
}

void mvn(String args) {
    sh "./mvnw --batch-mode -V -U -e ${args}"
}

Durch diese Änderungen werden die Mutation Tests im Anschluss an die Unit Tests automatisch ausgeführt, sowie ihre Reports in der ‘SonarQube’ Stage automatisch an SonarQube weitergereicht.

Die Dauer des Jenkins Builds hängt u.a. stark von der Größe des Projekts sowie der Anzahl der Tests ab. Ein Durchlauf der oben beschriebenen Pipeline sieht beispielsweise so aus:

Ergebnisse
Nach dem erfolgreichen Jenkins Build erfasst nun SonarQube überlebende Mutationen als Issues, und zeigt sie entsprechend an. Ein Ausschnitt aus den Issues für das PetClinic-Projekt kann dann so aussehen:

Falls es aktiviert wurde, ist nun auch das Pitest Widget mit Zahlen befüllt worden:

Von allen Mutationen (111) wurden 76,6% abgetötet, also insgesamt 85. Davon wurden 30 durch Unit-Tests, und 55 von Timeouts erwischt. Timeouts treten zum Beispiel auf, wenn durch eine Mutation eine Endlosschleife entsteht. Nachdem eine konfigurierbare Zeit abgelaufen ohne dass der Test fehlgeschlagen ist, markiert Pitest eine solche Mutation als ‘killed by timeout’.

Von den 26 überlebenden Mutationen wurden 16 von Unit-Tests nicht erkannt. Weitere 10 Mutationen befanden sich in von Unit-Tests nicht abgedeckten Stellen und konnten deshalb überhaupt nicht erkannt werden (‘10 non covered mutations’).

Für die Rate der getöteten Mutationen – also die 76,6% aus unserem Beispiel – gibt es keinen allgemeingültigen, als Goldstandard angesehenen Bestwert. Sicherlich ist eine möglichst hohe Mutation Coverage wünschenswert, allerdings ist sie immer in Relation mit dem dafür zu leistenden Aufwand zu bewerten.

Code
Wenn ihr unser Beispiel selbst nachvollziehen wollt, könnt ihr das PetClinic-Projekt inklusive den Änderungen am Jenkinsfile und der pom.xml aus unserem GitHub-Repository clonen.

Fazit

Der Aufwand, Pitest in SonarQube einzubinden, gestaltet sich dank des SonarQube-Pitest-Plugins erfreulich einfach. Auch die automatische Ausführung ist dank Jenkins kein Problem. Es fällt also schwer, einen Grund zu finden, Mutation Tests nicht beispielsweise jede Nacht ausführen zu lassen. Lediglich die Deutung der Ergebnisse gestaltet sich, aufgrund fehlender Eindeutigkeit, nicht ganz so einfach: Überlebende Mutationen sollten immer durch mindestens ein Paar menschlicher Augen darauf geprüft werden, ob sie tatsächlich Hinweise auf mangelhafte Unit-Tests geben. Das ist etwas, dass sich bisher nicht automatisieren lässt.

Diesen Beitrag teilen

Philipp Czora
Software Development
Philipp umgibt sich am liebsten mit Menschen, von denen er etwas lernen kann und versucht in der Softwareentwicklung die perfekte Mischung aus Pragmatismus und Perfektionismus zu finden.