Qualität im Build und eine CI-Pipeline: Checkstyle, Spotless, JaCoCo und GitLab CI

https://bildung.social/@oerinformatik/

https://oer-informatik.de/spring-boot-build-qualitaet-ci

Continuous Integration, GitLab CI, Pipeline, Checkstyle, Linter, Spotless, Formatierung, JaCoCo, Testabdeckung, Gradle, Maven, Spring Boot

tl/dr; (ca. 23 min Lesezeit): Tests helfen nur, wenn sie auch laufen. Der Build des Mitfahrlabors bekommt drei Prüfwerkzeuge: Checkstyle prüft Regeln für den Code, Spotless sorgt für einheitliche Formatierung, JaCoCo misst die Testabdeckung. Alle hängen an der Sammelaufgabe check (Gradle) bzw. der Phase verify (Maven). Eine Pipeline in GitLab CI führt bei jedem Push die Stufen build, test, quality und package aus und zeigt Testergebnisse und Abdeckung im Merge Request [AI-Assisted]. (Zuletzt geändert am 11.11.2026)

Dieser Artikel ist Teil einer Artikelreihe über ein REST-Backend mit Spring Boot: Überblick und Fahrplan / 01 Anforderungen und API-Vertrag / 02 Projekt anlegen / 03 Codegenerierung / 04 Fachlogik und Komponententest / 05 Build-Qualität und CI / Übungsaufgaben zu 05

Schon fit in diesem Thema? Nutze die Kann-Liste und die Leitfragen unter dem Artikel als Checkliste, ob Du die relevanten Kompetenzen bereits hast.

Dieser Artikel baut auf der Kann-Liste der Artikel Fachlogik und Komponententest, Build-Tool Gradle und CI/CD-Pipeline auf.

Als Build-Qualität-Grundlagen kann ich nach Durchlesen des Artikels…

  • … erklären, was ein Linter prüft und warum Teams sich auf Regeln einigen.

  • … den Unterschied zwischen einer Regelprüfung (Checkstyle) und einer Formatierungsprüfung (Spotless) beschreiben.

  • … eine Meldung von Checkstyle oder Spotless lesen und den Fehler im Code finden.

  • … Anweisungs- und Zweigabdeckung in einem JaCoCo-Bericht ablesen und deuten.

  • … die Prüfungen mit gradlew check bzw. mvnw verify ausführen und Formatierungsfehler automatisch beheben lassen.

  • … die Begriffe Pipeline, Stufe (stage), Job, Runner, Image, Artefakt und Cache erklären.

Darüber hinaus kann ich als Build-Qualität-Expertenwissen…

  • … Prüfwerkzeuge im Build konfigurieren und erzeugten Code von der Prüfung ausnehmen.

  • … eine .gitlab-ci.yml mit mehreren Stufen lesen, anpassen und Testberichte als Artefakte einbinden.

  • … begründen, warum eine hohe Testabdeckung kein Beleg für gute Tests ist.

Im letzten Artikel sind 40 Komponententests entstanden. Sie schützen aber nur, wenn jemand sie startet. Spätestens im Team vergisst das irgendwann jemand: Ein Commit mit rotem Test landet im Repository, und alle anderen arbeiten auf einem kaputten Stand weiter. Dazu kommen Fragen, die bei jeder Codebesprechung Zeit kosten: Tabulatoren oder Leerzeichen? Darf ein if ohne geschweifte Klammern stehen? Wie viel Code ist überhaupt getestet?

Dieser Artikel beantwortet diese Fragen ein für alle Mal, und zwar mit dem Build selbst. Drei Werkzeuge prüfen den Code, und eine Pipeline in GitLab führt die Prüfungen nach jedem Push automatisch aus. Im V-Modell gehört das zu keiner einzelnen Ebene: Die Pipeline ist das Gerüst, in das die Tests aller späteren Artikel eingehängt werden.

Fahrplan der Artikelreihe im V-Modell; unter der Implementierung ist Artikel 05 „CI-Pipeline prüft jeden Push“ hervorgehoben (mit KI erstellt)

Ausgangspunkt ist das Projekt aus Artikel 04. Das Ergebnis findest Du im Ordner src/05-build-qualitaet-ci des Repositorys. Wie Gradle und Maven grundsätzlich arbeiten, steht in den Artikeln Build-Tool Gradle und Build-Tool Maven; die Grundbegriffe von CI/CD erklärt der Artikel CI/CD-Pipeline.

Drei Werkzeuge, drei Fragen

Jedes Werkzeug beantwortet eine eigene Frage. Alle drei laufen als Teil des Builds, auf dem eigenen Rechner genauso wie später in der Pipeline:

Prüfwerkzeuge im Build des Mitfahrlabors
Werkzeug Frage Gradle-Aufgabe Maven-Ziel Ergebnis
Checkstyle Hält der Code die vereinbarten Regeln ein? checkstyleMain, checkstyleTest checkstyle:check Liste der Verstöße mit Datei, Zeile und Regel
Spotless Ist der Code einheitlich formatiert? spotlessCheck, spotlessApply spotless:check, spotless:apply Abweichungen als Diff; apply korrigiert sie
JaCoCo Welcher Code wird von Tests ausgeführt? jacocoTestReport jacoco:report Bericht in HTML und XML

Checkstyle ist ein Linter: ein Programm, das Quelltext liest, ohne ihn auszuführen, und nach verdächtigen Mustern sucht. Spotless kümmert sich nur um das Aussehen des Codes und kann es auch selbst korrigieren. JaCoCo (Java Code Coverage) misst dagegen beim Ausführen der Tests, welche Anweisungen durchlaufen wurden. Die ersten beiden sind statische Prüfungen, JaCoCo gehört zu den dynamischen.

Der Linter: Checkstyle mit eigenen Regeln

Checkstyle bringt fertige Regelsätze mit, etwa die Konventionen von Google oder Sun. Sie sind sehr umfangreich und melden in einem bestehenden Projekt oft Hunderte Verstöße, die niemand mehr liest. Für das Mitfahrlabor gibt es deshalb einen eigenen, kleinen Regelsatz in config/checkstyle/checkstyle.xml. Jede Regel hat einen Grund, der im Kommentar steht:

<module name="Checker">
    <!-- jeder Verstoß ist ein Fehler und bricht den Build ab -->
    <property name="severity" value="error"/>
    <property name="charset" value="UTF-8"/>

    <!-- Zeilen bleiben ohne Scrollen lesbar -->
    <module name="LineLength">
        <property name="max" value="120"/>
    </module>

    <module name="TreeWalker">
        <!-- Namenskonventionen -->
        <module name="TypeName"/>
        <module name="MethodName"/>
        <module name="ConstantName"/>
        <!-- … weitere Namensregeln -->

        <!-- Importe: keine Sternchen, keine doppelten -->
        <module name="AvoidStarImport"/>
        <module name="RedundantImport"/>

        <!-- typische Fehlerquellen -->
        <module name="NeedBraces"/>
        <module name="EqualsHashCode"/>
        <module name="StringLiteralEquality"/>
        <module name="EmptyCatchBlock"/>
        <!-- … -->

        <!-- Klassen nur mit statischen Methoden: privater Konstruktor -->
        <module name="HideUtilityClassConstructor"/>

        <!-- Methoden bleiben überschaubar -->
        <module name="MethodLength">
            <property name="max" value="40"/>
        </module>
        <module name="CyclomaticComplexity">
            <property name="max" value="10"/>
        </module>
    </module>
</module>

Der Checker ist die Wurzel. Regeln, die ganze Dateien betrachten (wie die Zeilenlänge), stehen direkt darunter; Regeln, die den Syntaxbaum des Java-Codes durchlaufen, stehen im TreeWalker. Einige Regeln verhindern echte Fehler: StringLiteralEquality meldet Vergleiche wie richtung == "zur-schule", die in Java Referenzen statt Inhalte vergleichen, und EqualsHashCode meldet Klassen, die equals() ohne hashCode() überschreiben. Andere sorgen für lesbaren Code, etwa die Namensregeln oder die Grenze von zehn Pfaden je Methode (CyclomaticComplexity).

Eingebunden wird Checkstyle über ein Plugin. Weil der Generator aus Artikel 03 Code erzeugt, der nicht unseren Regeln folgt (und den wir nicht ändern), nehmen beide Builds ihn aus:

In build.gradle.kts kommt checkstyle in den plugins-Block, dazu:

// Qualität 1: Checkstyle prüft Regeln aus config/checkstyle/checkstyle.xml
checkstyle {
    toolVersion = "14.4.0"
    maxWarnings = 0
}
tasks.withType<Checkstyle>().configureEach {
    exclude("de/dbh/mitfahrlabor/api/**") // erzeugter Code
}

Das Plugin legt die Aufgaben checkstyleMain und checkstyleTest an und sucht die Regeln selbst unter config/checkstyle/checkstyle.xml.

Der erste Lauf mit diesem Regelsatz findet genau einen Verstoß, und zwar in einer Datei, die niemand von uns geschrieben hat:

> Task :checkstyleMain
[ant:checkstyle] [ERROR] …/src/main/java/de/dbh/mitfahrlabor/MitfahrlaborApplication.java:6:1: Utility classes should not have a public or default constructor. [HideUtilityClassConstructor]

Die Startklasse vom Initializr hat nur die statische Methode main() und wirkt deshalb wie eine Hilfsklasse. Einen privaten Konstruktor darf sie aber nicht bekommen, denn Spring erzeugt von ihr ein Objekt. Hier irrt die Regel; man spricht von einem falsch positiven Befund. Statt die Regel ganz abzuschalten, bekommt sie eine Ausnahme für Klassen mit der Annotation @SpringBootApplication:

<!-- Ausnahme: die Startklasse braucht einen öffentlichen Konstruktor für Spring -->
<module name="HideUtilityClassConstructor">
    <property name="ignoreAnnotatedBy" value="SpringBootApplication"/>
</module>

Wie eine echte Meldung aussieht, zeigt ein Experiment: In Kostenregel.pruefe() werden die geschweiften Klammern um das throw weggelassen.

> Task :checkstyleMain
[ant:checkstyle] [ERROR] …/src/main/java/de/dbh/mitfahrlabor/domain/Kostenregel.java:28:9: 'if' construct must use '{}'s. [NeedBraces]

FAILURE: Build failed with an exception.
…
   > Checkstyle rule violations were found. See the report at: file:///…/build/reports/checkstyle/main.html
     Checkstyle files with violations: 1
     Checkstyle violations by severity: [error:1]

Jede Meldung nennt Datei, Zeile und Spalte (28:9), die Beschreibung und in eckigen Klammern den Namen der Regel. Unter diesem Namen findest Du die Regel in der Checkstyle-Dokumentation mit Begründung und Beispielen. Den ausführlichen Bericht schreibt Gradle nach build/reports/checkstyle/main.html, Maven nach target/checkstyle-result.xml.

Einheitliche Formatierung mit Spotless

Formatierung ist Geschmackssache, und genau deshalb sollte sie niemand von Hand pflegen. Wenn zwei Entwicklungsumgebungen unterschiedlich einrücken, ändert jeder Commit Zeilen, an denen sich inhaltlich nichts geändert hat, und im Diff gehen die echten Änderungen unter. Spotless legt die Formatierung im Build fest: spotlessCheck meldet Abweichungen, spotlessApply behebt sie.

Für das Mitfahrlabor reichen wenige, unstrittige Schritte: Importe in fester Reihenfolge (statische zuerst, dann java, org und der Rest, jeweils durch eine Leerzeile getrennt), keine ungenutzten Importe, Leerzeichen statt Tabulatoren am Zeilenanfang, keine Leerzeichen am Zeilenende und eine Leerzeile am Dateiende:

Im plugins-Block steht id("com.diffplug.spotless") version "8.11.0", dazu:

// Qualität 2: Spotless prüft (spotlessCheck) und korrigiert (spotlessApply) die Formatierung
spotless {
    java {
        target("src/*/java/**/*.java")
        importOrder("\\#", "java|javax", "org", "")
        removeUnusedImports()
        leadingTabsToSpaces(4)
        trimTrailingWhitespace()
        endWithNewline()
    }
}

target beschränkt Spotless auf die eigenen Quellordner. In importOrder steht \# (im Kotlin-Text als "\\#" geschrieben) für die statischen Importe und der leere Text für alle übrigen Pakete, hier de.

Wer mehr festlegen möchte, kann Spotless einen vollständigen Formatierer wie googleJavaFormat() oder palantirJavaFormat() vorschalten. Er bricht dann auch Zeilen um und setzt Klammern; das ändert in einem bestehenden Projekt allerdings fast jede Datei.

Der erste Lauf meldet zwei Dateien. Wieder stammen sie vom Initializr, der mit Tabulatoren einrückt, während der übrige Code Leerzeichen nutzt:

> Task :spotlessJavaCheck FAILED

* What went wrong:
Execution failed for task ':spotlessJavaCheck' (registered in build file 'build.gradle.kts').
> The following files had format violations:
      src/main/java/de/dbh/mitfahrlabor/MitfahrlaborApplication.java
          @@ -6,8 +6,8 @@
           @SpringBootApplication
           public·class·MitfahrlaborApplication·{
           
          -\tpublic·static·void·main(String[]·args)·{
          -\t\tSpringApplication.run(MitfahrlaborApplication.class,·args);
          -\t}
          +····public·static·void·main(String[]·args)·{
          +········SpringApplication.run(MitfahrlaborApplication.class,·args);
          +····}
           
           }
      src/test/java/de/dbh/mitfahrlabor/MitfahrlaborApplicationTests.java
          …
  Run './gradlew spotlessApply' to fix all violations.

Die Ausgabe ist ein Diff: Zeilen mit - stehen so in der Datei, Zeilen mit + so, wie sie sein sollten. Damit unsichtbare Zeichen sichtbar werden, zeigt Spotless Tabulatoren als \t und Leerzeichen als ·. Behoben wird das nicht von Hand:

.\gradlew.bat spotlessApply

Danach sind beide Dateien mit Leerzeichen eingerückt. Spotless ändert nur die Form, nie das Verhalten: Die Tests laufen vorher wie nachher. Trotzdem lohnt vor dem Commit ein Blick in die Änderungen, bevor sie zusammen mit inhaltlichen Änderungen in einem Commit landen.

Testabdeckung messen mit JaCoCo

Wie viel Code die Tests ausführen, misst JaCoCo. Dazu hängt es sich beim Testlauf als Java-Agent in die virtuelle Maschine und zählt, welche Bytecode-Anweisungen und welche Zweige von Verzweigungen durchlaufen werden. Nach dem Testlauf schreibt es einen Bericht. Auch hier bleibt der erzeugte Code außen vor, denn er hat keine eigenen Tests und würde die Zahlen verfälschen:

Im plugins-Block steht jacoco, dazu:

// Qualität 3: JaCoCo misst beim Testlauf die Abdeckung und schreibt einen Bericht
jacoco {
    toolVersion = "0.8.16"
}
tasks.test { finalizedBy(tasks.jacocoTestReport) }
tasks.jacocoTestReport {
    dependsOn(tasks.test)
    reports {
        xml.required = true
        html.required = true
    }
    classDirectories.setFrom(files(classDirectories.files.map {
        fileTree(it) { exclude("de/dbh/mitfahrlabor/api/**") }
    }))
}

finalizedBy sorgt dafür, dass der Bericht nach jedem Testlauf entsteht, auch nach einem roten. Den XML-Bericht braucht später die Pipeline.

Den Bericht öffnest Du nach dem Testlauf im Browser, bei Gradle unter build/reports/jacoco/test/html/index.html, bei Maven unter target/site/jacoco/index.html:

JaCoCo-Bericht des Mitfahrlabors nach Artikel 05: Paket domain mit 100 % Anweisungs- und Zweigabdeckung, Paket web mit 48 %, Startklasse mit 37 %, gesamt 73 % (83 von 315 Anweisungen nicht ausgeführt) und 100 % der Zweige

Die Spalten bedeuten:

Kennzahlen im JaCoCo-Bericht
Spalte Bedeutung Wert im Projekt
Missed Instructions / Cov. Anweisungsabdeckung (C0): ausgeführte Bytecode-Anweisungen 232 von 315 (73 %)
Missed Branches / Cov. Zweigabdeckung (C1): durchlaufene Zweige von if, switch und && 23 von 23 (100 %)
Cxty zyklomatische Komplexität: Zahl der unabhängigen Pfade 36, davon 7 nie durchlaufen
Lines Zeilen mit mindestens einer ausgeführten Anweisung 45 von 63
Methods, Classes mindestens einmal aufgerufene Methoden und Klassen 17 von 24, 12 von 12

Was die Kennzahlen genau messen und wie man sie von Hand bestimmt, erklärt der Artikel Überdeckungsmetriken. Hier zählt die Deutung: Das Paket domain aus Artikel 04 ist vollständig abgedeckt, jede Anweisung und jeder Zweig. Das Paket web erreicht nur 48 %. Ein Klick auf eine Klasse zeigt den Quelltext mit Farben:

Quelltext des AngebotController im JaCoCo-Bericht: Klassenkopf und Attribute grün hinterlegt, die Rümpfe von legeAngebotAn() und holeAngebot() rot

Grün sind Zeilen, deren Anweisungen ausgeführt wurden, rot solche, die kein Test erreicht hat. Gelb wären Zeilen mit einer Verzweigung, von der nur ein Teil der Zweige durchlaufen wurde. Der AngebotController ist grün, soweit der Smoke-Test die Anwendung startet und dabei ein Objekt erzeugt; seine Methoden ruft kein Test auf. Das ist ehrlich: Für die Web-Schicht gibt es noch keine Tests, sie folgen in Artikel 10.

Abdeckung ist ein Hinweis, kein Gütesiegel. Sie zeigt zuverlässig, was nicht getestet ist. Umgekehrt beweist eine grüne Zeile nur, dass sie ausgeführt wurde, nicht dass ein Test ihr Ergebnis geprüft hat. Ein Test ohne eine einzige Zusicherung erzeugt dieselbe Abdeckung wie ein guter. Deshalb setzt das Mitfahrlabor erst in Artikel 08, wenn die Service-Schicht steht, eine Mindestabdeckung als Hürde (quality gate) für den Build.

Alles zusammen: check und verify

Niemand soll sich fünf Befehle merken müssen. Gradle hat dafür die Sammelaufgabe check, von der alle Prüfungen abhängen (dependsOn). Die Plugins für Checkstyle und Spotless hängen sich selbst daran; openApiValidate wurde in Artikel 03 ergänzt, und JaCoCo läuft über finalizedBy immer im Anschluss an test:

Abhängigkeiten der Gradle-Aufgabe check: check hängt ab von test, checkstyleMain/checkstyleTest, spotlessCheck und openApiValidate; nach test folgt jacocoTestReport; jede Aufgabe schreibt einen Bericht unter build/reports oder eine Meldung ins Log (mit KI erstellt)

Maven kennt statt Aufgaben einen festen Lebenszyklus aus Phasen. Jedes Plugin-Ziel wird an eine Phase gebunden: jacoco:prepare-agent an den Anfang, jacoco:report an test, checkstyle:check und spotless:check an verify. Wer verify aufruft, durchläuft alle Phasen davor, also auch Kompilieren, Testen und Packen. Ein einziger Befehl prüft damit alles:

.\gradlew.bat check

Bei Erfolg endet Gradle mit BUILD SUCCESSFUL, Maven listet unterwegs die Ergebnisse und endet so:

[INFO] Tests run: 41, Failures: 0, Errors: 0, Skipped: 0
[INFO] --- jacoco:0.8.16:report (report) @ mitfahrlabor ---
[INFO] --- checkstyle:3.6.0:check (checkstyle) @ mitfahrlabor ---
[INFO] You have 0 Checkstyle violations.
[INFO] --- spotless:3.11.0:check (default) @ mitfahrlabor ---
[INFO] BUILD SUCCESS

Die 41 Tests sind die 40 Komponententests aus Artikel 04 und der Smoke-Test. Gewöhne Dir an, diesen Befehl vor jedem Push auszuführen. Ganz verlassen musst Du Dich darauf aber nicht mehr, denn ab jetzt prüft auch der Server.

Die Pipeline in GitLab CI

Continuous Integration (CI) bedeutet: Jede Änderung wird sofort in den gemeinsamen Stand integriert und dort automatisch gebaut und geprüft. In GitLab beschreibt die Datei .gitlab-ci.yml im Wurzelverzeichnis des Repositorys, was nach einem Push passiert. Die wichtigsten Begriffe:

Begriffe einer GitLab-Pipeline
Begriff Bedeutung im Projekt
Pipeline alle Schritte, die ein Push auslöst
Stufe (stage) Abschnitt der Pipeline; Stufen laufen nacheinander, eine rote Stufe stoppt die folgenden
Job eine Aufgabe mit eigenen Befehlen (script); Jobs einer Stufe laufen parallel
Runner Rechner, der die Jobs ausführt; auf gitlab.com stehen geteilte Runner bereit
Image Docker-Image, in dem ein Job läuft; hier eines mit Java 25
Artefakt Datei, die ein Job zum Herunterladen oder Anzeigen aufhebt, etwa Berichte oder die JAR-Datei
Cache Dateien, die zwischen Pipeline-Läufen erhalten bleiben, etwa heruntergeladene Bibliotheken

Die Pipeline des Mitfahrlabors hat vier Stufen mit je einem Job:

GitLab-Pipeline des Mitfahrlabors von links nach rechts: git push löst die Stufen build (compileJava, compileTestJava), test (test mit jacocoTestReport), quality (Checkstyle, spotlessCheck, openApiValidate) und package (bootJar) aus; test liefert Testbericht und Abdeckung für den Merge Request, quality bei Fehlern den Checkstyle-Bericht, package die JAR-Datei; bei einem Fehler wird die Pipeline rot und die folgenden Stufen laufen nicht (mit KI erstellt)

Die Reihenfolge ist Absicht: Wer nicht kompiliert, muss nicht getestet werden, und wer rote Tests hat, braucht keine JAR-Datei. Formatierung und Regeln stehen erst nach den Tests, damit ein vergessenes Leerzeichen nicht verdeckt, ob die Fachlogik stimmt. Die Datei für den Gradle-Build sieht so aus; die Maven-Fassung liegt im Projekt unter ci/maven.gitlab-ci.yml:

# Pipeline des Mitfahrlabor-Backends (Gradle)
# Gehört in das Wurzelverzeichnis des Projekt-Repositorys.
image: eclipse-temurin:25-jdk

variables:
  GRADLE_USER_HOME: "$CI_PROJECT_DIR/.gradle"
  GRADLE_OPTS: "-Dorg.gradle.daemon=false"

# heruntergeladene Bibliotheken zwischen den Läufen aufheben
cache:
  key: gradle-$CI_COMMIT_REF_SLUG
  paths:
    - .gradle/wrapper
    - .gradle/caches

stages:
  - build
  - test
  - quality
  - package

build:
  stage: build
  script:
    - ./gradlew compileJava compileTestJava

test:
  stage: test
  script:
    - ./gradlew test
  artifacts:
    when: always
    paths:
      - build/reports/tests/test/
      - build/reports/jacoco/test/html/
    reports:
      junit: build/test-results/test/TEST-*.xml
      coverage_report:
        coverage_format: jacoco
        path: build/reports/jacoco/test/jacocoTestReport.xml

quality:
  stage: quality
  script:
    - ./gradlew checkstyleMain checkstyleTest spotlessCheck openApiValidate
  artifacts:
    when: on_failure
    paths:
      - build/reports/checkstyle/

package:
  stage: package
  script:
    - ./gradlew bootJar
  artifacts:
    paths:
      - build/libs/*.jar
    expire_in: 1 week

Die Bausteine von oben nach unten:

  • image legt fest, in welchem Docker-Image alle Jobs laufen. eclipse-temurin:25-jdk enthält das JDK; Gradle bzw. Maven lädt der Wrapper selbst.

  • variables und cache sorgen dafür, dass heruntergeladene Bibliotheken im Projektordner landen und beim nächsten Lauf wiederverwendet werden. Ohne Cache lädt jeder Job alle Abhängigkeiten neu herunter, bei Spring Boot mehrere Hundert Megabyte.

  • stages legt die Reihenfolge fest; jeder Job nennt seine Stufe. Jeder Job startet in einem frischen Container und baut deshalb neu, was er braucht. Das kostet etwas Zeit, macht die Jobs aber unabhängig voneinander.

  • artifacts:reports liest GitLab selbst aus: Der junit-Bericht zeigt im Merge Request, welche Tests neu fehlschlagen. Mit coverage_report im Format jacoco markiert GitLab im Diff eines Merge Requests, welche geänderten Zeilen von Tests erreicht werden. Beide werden mit when: always auch dann hochgeladen, wenn der Job scheitert, denn gerade dann braucht man sie.

  • artifacts:paths hebt Dateien zum Herunterladen auf: den HTML-Bericht von JaCoCo, bei Fehlern den Checkstyle-Bericht und eine Woche lang die fertige JAR-Datei.

Zwei Stolpersteine gibt es beim ersten Lauf. Erstens muss die Datei im Wurzelverzeichnis des Repositorys liegen. Im Repository dieser Artikelreihe liegt das Projekt in einem Unterordner, dort ist die Datei ein Beispiel; für Dein eigenes Projekt kopierst Du den Ordner mitfahrlabor als Wurzel in ein neues Repository. Zweitens braucht gradlew bzw. mvnw das Ausführungsrecht. Unter Windows kennt Git dieses Recht nicht automatisch; setze es einmalig vor dem Commit mit git update-index --chmod=+x gradlew mvnw. Fehlt es, scheitert schon der erste Job mit der Meldung Permission denied.

Nach dem Push findest Du die Pipeline in GitLab unter Build > Pipelines. Ein Klick auf einen Job zeigt dieselbe Ausgabe, die der Befehl auf Deinem Rechner erzeugt; deshalb lässt sich jede rote Pipeline auch lokal nachstellen. Wie man eine rote Pipeline systematisch liest, zeigt der Artikel CI/CD-Pipeline.

Was die Pipeline (noch) nicht prüft

Die Pipeline prüft, was es an Prüfungen gibt, und das ist bisher wenig: Komponententests der Fachregeln und einen Smoke-Test. Ob die Anwendung mit einer echten Datenbank arbeitet, ob die Endpunkte den Vertrag einhalten oder ob die User Stories erfüllt sind, weiß sie nicht. Sie wächst deshalb mit: In Artikel 07 bekommt der Test-Job eine MariaDB-Datenbank als Dienst, in Artikel 08 kommt die Mindestabdeckung hinzu, in Artikel 11 System- und Abnahmetests und in Artikel 12 die Prüfung der Anmeldung.

Nächster Artikel

Die Haltepunkte und Angebote stehen bisher fest im Code oder nur im Arbeitsspeicher. Im nächsten Artikel bekommt das Mitfahrlabor eine Datenbank: Aus dem Fachmodell werden JPA-Entities mit Beziehungen, Spring Data erzeugt den Zugriff, und eine H2-Datenbank startet mit den Daten des Szenarios.

Leitfragen

  1. Was ist ein Linter, und worin unterscheiden sich Checkstyle und Spotless?

Ein Linter liest Quelltext, ohne ihn auszuführen, und meldet verdächtige Muster. Checkstyle prüft vereinbarte Regeln wie Namen, geschweifte Klammern oder Methodenlänge und meldet Verstöße, die man selbst beheben muss. Spotless prüft nur die Formatierung und kann sie mit spotlessApply selbst korrigieren. siehe Abschnitt „Drei Werkzeuge, drei Fragen“

  1. Wie liest man eine Checkstyle-Meldung, und was tut man bei einem falsch positiven Befund?

Die Meldung nennt Datei, Zeile und Spalte, eine Beschreibung und in eckigen Klammern den Namen der Regel, unter dem sie in der Dokumentation steht. Bei einem falsch positiven Befund schaltet man die Regel nicht ganz ab, sondern definiert eine gezielte Ausnahme, etwa ignoreAnnotatedBy für die Startklasse. siehe Abschnitt „Der Linter: Checkstyle mit eigenen Regeln“

  1. Warum wird der vom OpenAPI Generator erzeugte Code von Checkstyle und JaCoCo ausgenommen?

Er wird bei jedem Build neu erzeugt und nicht von Hand geändert, Verstöße ließen sich also nicht beheben. Außerdem hat er keine eigenen Tests und würde die Abdeckung verfälschen. siehe Abschnitt „Der Linter: Checkstyle mit eigenen Regeln“ und Abschnitt „Testabdeckung messen mit JaCoCo“

  1. Was bedeuten grüne, gelbe und rote Zeilen im JaCoCo-Bericht, und warum ist 100 % Abdeckung kein Beleg für gute Tests?

Grün: alle Anweisungen der Zeile wurden ausgeführt; gelb: von einer Verzweigung wurden nur einige Zweige durchlaufen; rot: kein Test hat die Zeile erreicht. Abdeckung misst nur, was ausgeführt wurde, nicht, ob ein Test das Ergebnis geprüft hat; ein Test ohne Zusicherung erzeugt dieselbe Abdeckung. siehe Abschnitt „Testabdeckung messen mit JaCoCo“

  1. Mit welchem Befehl führst Du alle Prüfungen aus, und wie hängen sie zusammen?

Mit gradlew check bzw. mvnw verify. Bei Gradle hängt check von test, den Checkstyle-Aufgaben, spotlessCheck und openApiValidate ab, jacocoTestReport folgt auf test. Bei Maven sind die Plugin-Ziele an Phasen gebunden, und verify durchläuft alle Phasen davor. siehe Abschnitt „Alles zusammen: check und verify“

  1. Welche Stufen hat die Pipeline des Mitfahrlabors, und warum stehen sie in dieser Reihenfolge?

build, test, quality und package. Was nicht kompiliert, muss nicht getestet werden; rote Tests sollen nicht von Formatierungsfehlern verdeckt werden; eine JAR-Datei entsteht nur aus einem geprüften Stand. Eine rote Stufe stoppt alle folgenden. siehe Abschnitt „Die Pipeline in GitLab CI“

  1. Was ist der Unterschied zwischen einem Artefakt und einem Cache?

Ein Artefakt ist ein Ergebnis eines Jobs, das GitLab zum Herunterladen aufhebt oder auswertet, etwa Testberichte oder die JAR-Datei. Ein Cache beschleunigt spätere Läufe, indem er Dateien wie heruntergeladene Bibliotheken zwischen den Pipelines aufhebt; auf ihn verlässt man sich nicht für Ergebnisse. siehe Abschnitt „Die Pipeline in GitLab CI“


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 4.0. Nennung gemäß TULLU-Regel bitte wie folgt: “Qualität im Build und eine CI-Pipeline: Checkstyle, Spotless, JaCoCo und GitLab CI” von oer-informatik.de (H. Stein), Lizenz: CC BY 4.0. Der Artikel wurde unter https://oer-informatik.de/spring-boot-build-qualitaet-ci veröffentlicht, die Quelltexte sind in weiterverarbeitbarer Form verfügbar im Repository unter https://gitlab.com/oer-informatik/java-springboot/Backend. Erstellt mit KI-Assistenz; von mir umfassend redaktionell überarbeitet, geprüft und freigegeben. Stand: 11.11.2026.

[Kommentare zum Artikel lesen, schreiben] / [Artikel teilen] / [gitlab-Issue zum Artikel schreiben]

Kommentare gerne per Mastodon, Verbesserungsvorschläge per gitlab issue (siehe oben). Beitrag teilen: