Parametrisierte Tests mit JUnit 5

Parametrisierte Tests mit JUnit 5

Gegeben ist folgende Klasse

public class IsIntegerPositive implements Predicate<Integer> {

    @Override
    public boolean test(Integer integer) {
        return integer != null && integer > 0;
    }

}

Nun möchte man mehrere positive Werte testen. Da die Testmethode doch sehr überschaubar ist, würde man in der Regel zur folgenden Lösung neigen:

@Test
void testIsPositive() {
    assertTrue(underTest.test(1));
    assertTrue(underTest.test(100));
    assertTrue(underTest.test(10_000));
}

Unabhängig von der Diskussion, ob gute Tests nur eine Assertion haben sollten, ließe sich dieser Test wunderbar parametrisiert lösen:

@ParameterizedTest
@ValueSource(ints = { 1, 100, 10_000 })
void testPositives(Integer integer) {
    assertTrue(underTest.test(integer));
}

Die ParameterizedTest-Annotation markiert die Methode als Test und ersetzt die Test-Annotation. In @ValueSource sind die Daten angegeben, die jeweils als Parameter an die Methode übergeben wird. Für jeden angegebenen Wert wird der Test nun ein Mal ausgeführt. IntelliJ zeigt diese auch gesondert an

Parametrisierte Tests mit JUnit 5

Für das Testen von negativen Werten schreibt man nun noch eine entsprechende Methode mit negativen Werten als Parameter.

@ParameterizedTest
@ValueSource(ints = { -1, -100, -10_000 })
void testNegatives(Integer integer) {
    assertFalse(underTest.test(integer));
}

Um abschließend noch null und 0 zu testen können wird dem ValueSource-Argument ints nicht einfach null übergeben, sondern müssen die zusätzliche Annotation NullSource nutzen:

@ParameterizedTest
@ValueSource(ints = 0)
@NullSource
void testNullAndZero(Integer integer) {
    assertFalse(underTest.test(integer));
}

Parameter als CSV

Gegeben ist folgende Klasse

public class UpperCaseString implements UnaryOperator<String> {

    @Override
    public String apply(String string) {
        return string.toUpperCase();
    }

}

Nun möchte man dem Test neben den zu testenden String auch den erwarteten Rückgabewert übergeben - sprich, man möchte der Testmethode mehrere Parameter übergeben. Da dies @ValueSource nicht möglich ist, nutzen wir dazu das CSV-Format mit entsprechender Annotation.

@ParameterizedTest
@CsvSource({ "foo,FOO", "bAr,BAR", "f00 b4r,F00 B4R" })
void apply(String input, String expected) {
    assertEquals(expected, underTest.apply(input));
}

Das Ganze kann noch etwas übersichtlicher gestaltet werden, indem man die Testdaten aus einer passenden CSV-Datei aus dem resources-Verzeichnis laden lässt.

input,expected
foo,FOO
bAr,BAR
f00 b4r,F00 B4R

Die erste Zeile der CSV-Datei lässt sich durch das Setzten des numLinesToSkip-Arguments ignorieren.

@ParameterizedTest
@CsvFileSource(resources = "/upper-case-string-data.csv", numLinesToSkip = 1, delimiter = )
void apply_fromFileSource(String input, String expected) {
    assertEquals(expected, underTest.apply(input));
}
@ParameterizedTest
@CsvFileSource(resources = "/upper-case-string-data.csv", numLinesToSkip = 1, delimiter = )
void apply_fromFileSource(String input, String expected) {
    assertEquals(expected, underTest.apply(input));
}

Methode als Parameterquelle

Gegeben ist folgende Klasse

public class ConcatStrings implements Function<List<String>, String> {

    @Override
    public String apply(List<String> strings) {
        if (strings == null) {
            return "";
        }

        return String.join("", strings);
    }

}

Mit der MethodSource-Annotation kann eine statische Methode als Datenquelle genutzt werden. Diese Methode muss dann einen Stream von Arguments zurückgeben. Das bietet sich bspw. an, wenn komplexere Datentypen als Testparameter genutzt werden sollen.

private static Stream<Arguments> provideStrings() {
    return Stream.of(
            Arguments.of(List.of("foo", "bar"), "foobar"),
            Arguments.of(List.of(), ""),
            Arguments.of(null, "")
    );
}

Die Source-Methode wird dann einfach der MethodSource-Annotation als value-Argument übergeben.

@ParameterizedTest
@MethodSource("provideStrings")
void apply(List<String> input, String expected) {
    assertEquals(expected, underTest.apply(input));
}

Fazit

Mit parametrisierten Tests lassen sich Testklassen wesentlich einfacher und übersichtlicher gestalten. Kopierte Testmethoden mit angepassten Daten lassen sich somit komplett eliminieren und auch eine extrem starke Abstrahierung (sodass die Test-Suites gerne schon eine völlig eigene Businesslogik haben) kann so vermieden werden. Das hier war natürlich nur ein kleiner Einblick in das, was mit @ParameterizedTest möglich ist. Detaillierte Informationen sind in der JUnit-Dokumentation zu finden.

Ein ausführbares Beispielprojekt lässt sich auf meinem GitHub-Account finden: github.com/pklink/article-junit5-parameteri..


(Das Artikelfoto stammt von Biblioteca de Arte / Art Library Fundação Calouste Gulbenkian)