Start / Cheat Sheets

Cheat Sheet

JUnit 6 — Cheat Sheet

Stand: · Java Testing mit JUnit, Mockito und Testcontainers

JUnitJavaAssertJTestautomatisierung

Der Spickzettel zum Testhandwerk aus dem Seminar Java Testing mit JUnit, Mockito und Testcontainers — Artefaktnamen, Annotationen und Konfigurations­ schlüssel zum Nachschlagen, wenn man weiß, dass es geht, aber nicht mehr genau wie. Für Attrappen und Container gibt es ein eigenes Blatt: Mockito & Testcontainers. Maßgeblich und immer aktuell ist das JUnit 5/6 User Guide; hier steht die Auswahl, die im Alltag tatsächlich gebraucht wird.

Testebenen und was sie kosten

Zwei Blicke auf dieselbe Entscheidung: Was findet eine Ebene — und was verlangt sie dafür.

EbeneFindetFindet nichtLaufzeitPflege
UnitRechenfehler, Grenzfällefalsche VerdrahtungMillisekundenmittel
KomponenteVertragsbrüche zwischen Klassenechtes SQLZehntelsekundengering
IntegrationSchema, Mapping, TransaktionenZusammenspiel der DiensteSekundenmittel
End-to-Endden kaputten Gesamtwegdie UrsacheMinutenhoch

Ein Fehler, der auf zwei Ebenen geprüft wird, kostet doppelt und meldet sich doppelt. Und Pflegeaufwand schlägt Laufzeit: Ein Test, den niemand mehr repariert, wird abgeschaltet.

Quer dazu läuft eine zweite Trennung, die im Alltag mehr trägt als jede Pyramide: fachliche Testfälle kommen aus der Domäne und überleben jedes Refactoring, technische kommen aus der Umsetzung und sterben mit ihr. Wer beide vermischt, kann bei rotem Test nicht sagen, ob die Regel oder der Code falsch war.

Modul: Eine tragfähige Teststrategie entwickeln

Artefakte und Build-Einbindung

ArtefaktGruppeRolle
junit-bomorg.junithält alle Versionen konsistent
junit-jupiterorg.junit.jupiterAPI und Engine als Bündel
junit-jupiter-paramsorg.junit.jupiterparametrisierte Tests
junit-platform-launcherorg.junit.platformStart aus Build und IDE
junit-vintage-engineorg.junit.vintageführt JUnit-4-Tests aus

In JUnit 6 trägt nur die BOM die Gruppe org.junit, die Komponenten behalten ihre eigenen Gruppen. Baseline ist Java 17.

<dependency>
  <groupId>org.junit</groupId>
  <artifactId>junit-bom</artifactId>
  <version>6.1.3</version>
  <type>pom</type>
  <scope>import</scope>
</dependency>
testImplementation(platform("org.junit:junit-bom:6.1.3"))
testImplementation("org.junit.jupiter:junit-jupiter")
testRuntimeOnly("org.junit.platform:junit-platform-launcher")

Fehlt der Launcher, meldet Gradle lapidar, es gebe keine Tests.

Konfiguration statt Code

ParameterWirkung
junit.jupiter.execution.parallel.enabledschaltet parallele Ausführung frei
junit.jupiter.testinstance.lifecycle.defaulteine Instanz je Test oder je Klasse
junit.jupiter.displayname.generator.defaultRegel für die angezeigten Namen
junit.jupiter.extensions.autodetection.enabledExtensions über ServiceLoader laden

Die Parameter stehen in junit-platform.properties im Testklassenpfad — liegt die Datei im Hauptquellbaum, wirkt sie nicht.

Modul: JUnit 6 verstehen und einrichten

Von JUnit 4 nach Jupiter

ThemaJUnit 4Jupiter
Vor jedem Test@Before@BeforeEach
Erwartete Exception@Test(expected = …)assertThrows
ErweiterungRunner und RuleExtension
Ignorieren@Ignore@Disabled
ParametrisierungRunner mit Parametern@ParameterizedTest

Die Umbenennungen erledigt ein Rezept der Migrationswerkzeuge, die Extensions bleiben Handarbeit. Vintage ist eine Brücke, kein Ufer: Rules und Runner lassen sich nicht mit Extensions mischen, und solange Vintage läuft, gibt es zwei Programmiermodelle im selben Projekt.

Modul: JUnit 6 verstehen und einrichten

Assertions, die etwas aussagen

PrüfungWerkzeug
Ein WertassertEquals, erwarteter Wert zuerst
Mehrere Werte eines ObjektsassertAll mit einem Namen für die Gruppe
Erwartete AusnahmeassertThrows, danach die Meldung prüfen
LaufzeitgrenzeassertTimeout, nicht ein eigener Zeitstempel
assertAll("Bestellung",
  () -> assertEquals(OFFEN, bestellung.status()),
  () -> assertEquals(3, bestellung.zeilen().size()));

var fehler = assertThrows(LieferfensterFehler.class,
    () -> bestellung.liefereAm(SONNTAG));
assertEquals("Sonntag wird nicht geliefert", fehler.getMessage());

assertThrows gibt die Ausnahme zurück — erst damit lässt sich die Meldung prüfen. Und die Reihenfolge zählt: erwarteter Wert links, tatsächlicher rechts, sonst steht die Meldung im Bericht auf dem Kopf.

Der Unterschied wird sichtbar, wenn ein Test nach einem Jahr das erste Mal bricht:

SchreibweiseMeldung im Fehlerfall
assertTrue mit Vergleicherwartet true, war false
assertEqualserwartet 42.90, war 39.90
assertThat mit AssertJerwartet 42.90, war 39.90, Abweichung 3.00
assertThat(bestellung.zeilen())
    .hasSize(3)
    .extracting(Zeile::artikel)
    .containsExactly("Apfelkiste", "Honig", "Möhren");
assertThat(bestellung.gesamt()).isEqualByComparingTo(euro("42.90"));

Bei Beträgen isEqualByComparingTo statt isEqualTo — sonst vergleicht der Test die Skalierung statt den Wert.

Testnamen und Testschnitt

Nach Methode geschnittenNach Verhalten geschnitten
rechneTest1, rechneTest2staffelpreisAbZehnKisten
bricht bei jeder Umbenennungüberlebt das Refactoring
prüft, was der Code tutprüft, was gelten soll
wächst mit der Klassewächst mit der Fachlichkeit

@DisplayName darf Umlaute, Leerzeichen und ganze Sätze enthalten — der Name ist die halbe Fehlermeldung, und der Bericht wird dadurch dem Fachbereich zeigbar.

Modul: Verständliche Unit-Tests schreiben

Parametrisierte Tests und Testdaten

QuellePasst für
@ValueSourceeine einzelne Reihe einfacher Werte
@CsvSourcemehrere Spalten, direkt im Test lesbar
@CsvFileSourcegrößere Datensätze aus einer Datei
@MethodSourceArgumente, die berechnet oder Objekte sind
@FieldSourcevorbereitete Listen in einem statischen Feld
@EnumSourcealle oder ausgewählte Konstanten einer Enum
@ParameterizedTest(name = "{0} Kisten je {1} Euro")
@CsvSource({ "1, 18.00", "9, 18.00", "10, 17.10", "50, 16.20" })
void staffelpreisJeKiste(int menge, BigDecimal preis) { … }

Der name-Platzhalter macht aus vier Läufen vier lesbare Zeilen im Bericht — ohne ihn steht dort nur der Index. Objekte gehören in @MethodSource, nicht in @CsvSource mit einem Converter, der die Hälfte errät; für lange Signaturen gibt es ArgumentConverter, ArgumentsAggregator und ArgumentsAccessor.

BauartFälle stehen festPasst für
@ParameterizedTestbeim Schreibenbekannte Datenreihen
@ParameterizedClassbeim Schreibendieselbe Klasse je Konstellation
@TestFactoryzur LaufzeitFälle aus Datei oder Datenbank

Dynamische Tests sind mächtig und unübersichtlich — die letzte Wahl, nicht die erste.

Für Grenzfälle gilt: Jede Regel ergibt zwei Fälle — den letzten ungültigen und den ersten gültigen Wert. Die Mitte einer Äquivalenzklasse zu testen kostet Zeit und findet nichts.

RegelLetzter ungültiger WertErster gültiger Wert
Mindestbestellwert24.99 Euro25.00 Euro
Staffelpreis9 Kisten10 Kisten
Vorlauf Lieferungheutemorgen
GutscheinlaufzeitVortag des StartsStarttag

Dazu gehören null und leer — sie gelten oft als Sonderfall und tauchen dann in keinem Test auf.

@Nested gruppiert nach Vorbedingung, nicht nach Codestruktur: Der Klassenname wird zur Bedingung, der Methodenname zur Erwartung.

Modul: Testdaten und unterschiedliche Szenarien

Lifecycle und eingebaute Erweiterungen

AnnotationWirkung
@TempDirlegt ein temporäres Verzeichnis an und räumt es weg
@AutoCloseschließt eine Ressource im Feld nach dem Test
@DefaultLocalesetzt die Standardsprache für die Testdauer
@DefaultTimeZonesetzt die Standardzeitzone für die Testdauer
@SetSystemPropertysetzt eine Systemeigenschaft und stellt sie zurück
@ClearSystemPropertyentfernt eine Systemeigenschaft für die Testdauer

Locale, Zeitzone und Systemeigenschaften kamen mit JUnit 6.1 dazu — vorher brauchte es eine Fremdbibliothek. Ohne sie entscheidet der Rechner des Entwicklers über das Ergebnis.

@Test
@DefaultLocale("de-DE")
@DefaultTimeZone("Europe/Berlin")
void rechnungMitDeutschemDatum(@TempDir Path ordner) { … }

Bedingte Ausführung über @EnabledOnOs und @EnabledOnJre ist ehrlicher als ein auskommentierter Test: Ein übersprungener Test steht im Bericht, ein auskommentierter verschwindet. Eigene Bedingungen entstehen als ExecutionCondition, nicht als if im Testcode.

Eine eigene Extension implementiert die passende Schnittstelle — für einen Testparameter etwa ParameterResolver mit supportsParameter und resolveParameter, eingebunden über @ExtendWith(FesteUhr.class). Zustand gehört dabei in den ExtensionContext.Store, nie in ein Feld: Ein Feld wird bei paralleler Ausführung geteilt. Was im Store liegt und AutoCloseable ist, räumt JUnit selbst wieder ab.

Parallele Ausführung

ParameterWerte
junit.jupiter.execution.parallel.enabledtrue, false
junit.jupiter.execution.parallel.mode.defaultconcurrent, same_thread
junit.jupiter.execution.parallel.config.strategydynamic, fixed, custom
junit.jupiter.execution.parallel.config.executor-servicefork_join_pool, worker_thread_pool

Der Worker-Thread-Pool kam mit JUnit 6.1 dazu und gilt noch als experimentell. Am Test steuern statt global: @Execution(ExecutionMode.CONCURRENT), @ResourceLock(Resources.SYSTEM_PROPERTIES) für geteilte Ressourcen und @Isolated als grobe Kelle — die Klasse läuft allein, alles andere wartet.

Modul: Lifecycle, Extensions und parallele Ausführung

Typische Fallen

  • Jeden neuen Fehler auf der Ebene testen, auf der er zufällig auffiel.
  • Die Pyramide als Quote verordnen, ohne das Risiko der Anwendung anzusehen.
  • Langsame Tests aus der Pipeline nehmen, statt sie zu beschleunigen.
  • Jede Abhängigkeit mit eigener Version führen — dann steht die BOM zur Zierde.
  • Vintage dauerhaft drinlassen, obwohl nur noch drei JUnit-4-Tests existieren.
  • Tags vergeben, ohne dass ein Build danach filtert.
  • assertTrue auf einen Vergleich setzen — die Meldung sagt dann nur „war false”.
  • Erwarteten und tatsächlichen Wert vertauschen und die Meldung umdrehen.
  • assertThrows um drei Zeilen legen, wenn die Ausnahme aus der ersten kam.
  • AssertJ und Jupiter-Assertions mischen in derselben Klasse.
  • Beträge als double prüfen und an der Rundung scheitern.
  • @BeforeAll nicht statisch deklarieren, ohne den Lifecycle umzustellen.
  • PER_CLASS wählen, um Zustand zu sparen — und Tests beeinflussen sich.
  • Zustand im Feld einer Extension halten; das funktioniert nur seriell.
  • Parallelität einschalten und die Flaky-Analyse teurer bezahlen als die gesparten zehn Prozent Laufzeit.

Zum Seminar Java Testing mit JUnit, Mockito und Testcontainers