Java JUnit Parametrisierte Tests
Führe denselben JUnit-Test mit verschiedenen Eingaben via @ParameterizedTest und Wertquellen aus.
Ein parametrisierter Test führt dieselbe Testmethode mehrmals aus – einmal für jeden Satz von Eingaben, den du ihm übergibst. Anstatt testReverseAbc, testReverseEmpty und testReverseSingle zu kopieren, schreibst du die Logik einmal und gibst eine Datenquelle an – eine Liste von Eingaben und erwarteten Ergebnissen. JUnit 5 (die Jupiter-Engine) macht dies mit @ParameterizedTest und einer Familie von Quell-Annotationen erstklassig. Der Vorteil: weniger Zeilen, dichtere Abdeckung und jede Eingabe wird als eigenes Bestanden/Nicht-bestanden-Ergebnis angezeigt.
Dieses Kapitel setzt voraus, dass du bereits weißt, wie ein einfacher Test geschrieben und geprüft wird; falls nicht, starte mit der JUnit-Einführung und den JUnit-Assertions. Es behandelt, wann ein parametrisierter Test sinnvoll ist, wie man eine Argumentquelle auswählt (@ValueSource, @CsvSource, @MethodSource und andere) und den häufigsten Fehler – ein falscher erwarteter Wert statt eines Code-Fehlers.
Von wiederholten Tests zu einem parametrisierten Test
Eine einfache @Test-Methode testet genau ein Szenario. Wenn du dasselbe Verhalten über eine Tabelle von Eingaben prüfen möchtest, wiederholt der naive Ansatz die Methode:
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertTrue;
class PrimesTest {
@Test void two_isPrime() { assertTrue(Primes.isPrime(2)); }
@Test void seven_isPrime() { assertTrue(Primes.isPrime(7)); }
@Test void thirteen_isPrime() { assertTrue(Primes.isPrime(13)); }
}Die parametrisierte Version fasst alle drei in eine Methode zusammen. Du annotierst mit @ParameterizedTest (nicht @Test) und hängst eine Quelle an, die das Argument für jeden Durchlauf liefert:
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;
import static org.junit.jupiter.api.Assertions.assertTrue;
class PrimesTest {
@ParameterizedTest
@ValueSource(ints = {2, 7, 13})
void isPrime(int candidate) {
assertTrue(Primes.isPrime(candidate));
}
}JUnit ruft isPrime dreimal auf – candidate=2, dann 7, dann 13 – und meldet drei Ergebnisse. Ein fehlschlagender Wert verdeckt die anderen nicht.
Eine Argumentquelle auswählen
Die Annotation @ParameterizedTest ist allein nutzlos; sie benötigt eine Quelle, die die Argumente liefert. JUnit Jupiter stellt mehrere bereit, jede für eine andere Datenform geeignet:
| Quelle | Liefert | Am besten für |
|---|---|---|
@ValueSource | Einen einzelnen Literalwert pro Durchlauf (ints, strings, doubles, …) | Einargument-Tests |
@CsvSource | Eine Zeile kommagetrennte Werte pro Durchlauf | Wenige Inline-Zeilen mit mehreren Spalten |
@CsvFileSource | Zeilen aus einer .csv-Datei im Klassenpfad | Große oder extern gepflegte Tabellen |
@MethodSource | Was auch immer eine Factory-Methode als Stream/Collection zurückgibt | Komplexe Objekte, berechnete Fälle |
@EnumSource | Die Konstanten eines Enums | Vollständige Abdeckung eines Enums |
@NullSource / @EmptySource | null- und Leer-Werte | Grenzfall-Abdeckung für Strings/Collections |
Als Faustregel gilt: @ValueSource für eine einfache Eingabe, @CsvSource für eine kleine mehrspaltige Tabelle und @MethodSource, sobald die Daten nicht mehr in Annotationsliterale passen.
Mehrere Spalten mit @CsvSource
Wenn jeder Fall eine Eingabe und eine erwartete Ausgabe hat, bietet @CsvSource eine kleine Inline-Tabelle. Jeder String ist eine Zeile; Kommas teilen ihn in geordnete Methodenparameter auf:
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvSource;
import static org.junit.jupiter.api.Assertions.assertEquals;
class StringsTest {
@ParameterizedTest
@CsvSource({
"abc, cba",
"racecar, racecar",
"'', ''" // single quotes denote an empty string
})
void reverse(String input, String expected) {
assertEquals(expected, Strings.reverse(input));
}
}JUnit konvertiert jedes kommagetrennte Token in den deklarierten Parametertyp, sodass @CsvSource({"4, 16"}) in (int n, int square) landen kann. Verwende einfache Anführungszeichen, um Kommas oder leere Strings in einer Zelle einzuschließen.
Berechnete Fälle mit @MethodSource
Annotationswerte müssen Compile-Time-Konstanten sein. Sobald Argumente echte Objekte sind oder berechnet werden müssen, wechsle zu @MethodSource. Es benennt eine statische Methode, die einen Stream<Arguments> (oder eine beliebige Collection/Array) zurückgibt:
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
import java.util.stream.Stream;
import static org.junit.jupiter.api.Assertions.assertEquals;
class TaxTest {
static Stream<Arguments> brackets() {
return Stream.of(
Arguments.of(0, 0.0),
Arguments.of(10_000, 1_000.0),
Arguments.of(50_000, 7_500.0)
);
}
@ParameterizedTest(name = "income {0} -> tax {1}")
@MethodSource("brackets")
void computesTax(int income, double expectedTax) {
assertEquals(expectedTax, Tax.of(income));
}
}Das optionale name-Attribut passt an, wie jede Ausführung im Testbericht erscheint, wobei {0}, {1} für die Argumente stehen – unverzichtbar, wenn eine einzelne fehlschlagende Zeile auf einen Blick identifiziert werden muss.
Ein vollständiges Beispiel: ein parametrisierter Runner ohne JUnit
Der Code-Runner hat kein JUnit im Klassenpfad, daher modelliert dieses Programm den Mechanismus, den ein parametrisierter Test verkörpert, mit einfachem JDK-Code: Eine einzelne Prüfung wird einmal definiert und dann über eine Liste von Fällen ausgeführt – genau das, was @ParameterizedTest hinter den Annotationen tut. Ein Fall ist absichtlich falsch, damit du sehen kannst, wie einzelne Zeilen bestehen oder fehlschlagen.
Was man aus dem Durchlauf mitnehmen kann:
- Der
reverse-Block gibt vierPASS-Zeilen und>> reverse: 4 passed, 0 failedaus – ein Methodenrumpf (reverse) lief gegen vier Zeilen, was widerspiegelt, wie eine einzelne@ParameterizedTest-Methode einmal pro@CsvSource-Zeile aufgerufen wird. - Der
isPrime-Block gibtPASSfür die Eingaben2,7,9und1aus, aberFAILfür Eingabe4, weilisPrime(4)falsezurückgibt, während die Zeiletruebehauptete – eine falsche Erwartung, kein Code-Fehler, was der häufigste Fehler bei parametrisierten Tests ist. - Dieser einzelne Fehlschlag wird in einer eigenen Zeile gemeldet und als
>> isPrime: 4 passed, 1 failedgezählt; die anderen Zeilen bestehen weiterhin, was den Hauptvorteil gegenüber einer handgefertigten Schleife mit einer Assertion demonstriert – jede Eingabe ist ein unabhängiger, individuell gemeldeter Fall. - Der
runAll-Helfer nimmt die Einheit alsFunctionund die Fälle alsList, wodurch die zu testende Logik von den Daten getrennt wird – genau die Trennung, die@ParameterizedTestzusammen mit einer Argumentquelle bietet. - Jede Zeile zeigt
expectednebenactual, sodass die Zeile4 / expected=true / actual=falsegenau angibt, welcher Wert nicht übereinstimmte – den gleichen Diagnosewert liefern JUnitsassertEquals-Meldung und dasname = "..."-Template.
Wann ein parametrisierter Test sinnvoll ist
Greife zu @ParameterizedTest, wenn ein Verhalten über eine Tabelle von Eingaben gelten soll – Grenzwerte, Äquivalenzklassen oder eine Regressionsliste von Eingaben, die einmal fehlgeschlagen sind. Verwende weiterhin einen einfachen @Test, wenn ein Szenario ein einzigartiges Setup oder eigene Assertions benötigt; unzusammenhängende Fälle in eine parametrisierte Methode zu quetschen, macht den Bericht nur schwerer lesbar. Für gemeinsames Setup bei beiden Stilen, siehe das Kapitel zum Test-Lifecycle, und für das vollständige Assertion-Vokabular, das innerhalb jedes Durchlaufs verwendet wird, das Kapitel zu Assertions.