22.2 Das Test-Framework JUnit
Oracle definiert kein allgemeines Standard-Framework zur Definition von Unit-Testfällen, und es bietet auch keine Ablaufumgebung für Testfälle. Diese Lücke füllen Test-Frameworks, wobei das populärste im Java-Bereich das freie quelloffene JUnit (http://junit.org/junit5/) ist. Mehr als 60 % aller quelloffenen Projekte unter GitHub referenzieren diese Bibliothek.
JUnit-Versionen
Kent Beck und Erich Gamma begannen im Jahr 2000 mit der Entwicklung des JUnit-Frameworks. Die aktuellen Änderungen kommen von diversen Entwicklern. Der ursprüngliche Zweig JUnit 3 nutzt keine Annotationen. Das änderte sich 2006 mit der Version JUnit 4, die aktuell die größte Nutzerbasis hat. Da JUnit 4 eine monolithische Bibliothek ist, wurde JUnit 5 geschrieben und eine Modularisierung vorgenommen: JUnit 5 besteht aus den Teilen JUnit Platform, JUnit Jupiter und JUnit Vintage. JUnit 5 benötigt mindestens Java 8 und benennt auch Annotationen um, sodass es einen gewissen Migrationsaufwand bei der Umstellung von JUnit 4 gibt.
JUnit aufnehmen
Wir wollen mit der aktuellen Version JUnit 5 arbeiten. Da JUnit kein Teil der Java SE ist, müssen wir Bibliotheken im Klassenpfad mit aufnehmen. Wir können natürlich die JAR-Dateien von Hand herunterladen und in den Klassenpfad aufnehmen, doch über Maven und die IDE geht es einfacher und schneller. Wir fügen folgende Abhängigkeit in der POM-Datei ein:
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-engine</artifactId>
<version>5.6.2</version>
<scope>test</scope>
</dependency>
Die Standard-IDEs Eclipse und IntelliJ bringen JUnit gleich mit und bieten Wizards an, mit denen sich einfach Testfälle aus vorhandenen Klassen erstellen lassen. Per Tastendruck lassen sich Testfälle abarbeiten, und ein farbiger Balken zeigt direkt an, ob wir unsere Arbeit gut gemacht haben.
22.2.1 Test-Driven Development und Test-First
Unser JUnit-Beispiel wollen wir nach einem ganz speziellen Ansatz entwickeln, der sich Test-First nennt. Dabei wird der Testfall noch vor der eigentlichen Implementierung geschrieben. Die Reihenfolge mit dem Test-First-Ansatz sieht (etwas erweitert) so aus:
Überlege, welche Klasse und Methode geschrieben werden soll. Lege Quellcode für die Klasse und für die Variablen, Methoden bzw. Konstruktoren an, sodass sich die Compilationseinheit übersetzen lässt. Die Codeblöcke sind leer, enthalten aber mitunter eine return-Anweisung mit Rückgabe, sodass die Typen und Methoden bzw. Konstruktoren »da« sind, aber keine Funktionalität besitzen.
Schreibe die API-Dokumentation, und dokumentiere, welche Funktion und Bedeutung Parameter, Rückgaben und Ausnahmen haben.
Teste die API an einem Beispiel, das zeigt, ob sich die Klasse mit Eigenschaften »natürlich« anfühlt. Falls nötig, wechsle zu Punkt 1, und passe die Eigenschaften an.
Implementiere eine Testklasse.
Implementiere die Logik des eigentlichen Programms.
Gibt es durch die Implementierung neue Dinge, die ein Testfall testen sollte? Wenn ja, erweitere den Testfall.
Führe die Tests aus, und wiederhole ab Schritt 5, bis alles fehlerfrei läuft.
Der Test-First-Ansatz hat den großen Vorteil, dass er überschnelle Entwickler, die ohne groß zu denken zur Tastatur greifen, dann implementieren und nach 20 Minuten wieder alles ändern, zum Nachdenken zwingt. Große Änderungen kosten Zeit und somit Geld, und Test-First verringert die Notwendigkeit späterer Änderungen. Denn wenn Entwickler Zeit in die API-Dokumentation investieren und Testfälle schreiben, dann haben sie eine sehr gute Vorstellung von der Arbeitsweise der Klasse und große Änderungen sind seltener.
Der Test-First-Ansatz ist eine Anwendung von Test-Driven Development (TDD). Hier geht es darum, die Testbarkeit gleich als Ziel bei der Softwareentwicklung zu definieren. Hieran krankten frühere Entwicklungsmodelle, etwa das wohlbekannte Wasserfallmodell, das das Testen an das Ende – nach Analyse, Design und Implementierung – stellte. Die Konsequenz dieser Reihenfolge war oft ein großer Klumpen Programmcode, der unmöglich zu testen war. Mit TDD soll das nicht mehr passieren. Heutzutage sollten sich Entwickler bei jeder Architektur, jedem Design und jeder Klasse gleich zu Beginn überlegen, wie das Ergebnis zu testen ist. Untersuchungen zeigen: Mit TDD ist das Design signifikant besser.
Zu der Frage, wann Tests durchgeführt werden sollen, lässt sich nur eines festhalten: so oft wie möglich. Denn je eher ein Test durch eine falsche Programmänderung fehlschlägt, desto eher kann der Fehler behoben werden. Gute Zeitpunkte sind daher vor und hinter größeren Designänderungen und auf jeden Fall vor dem Einpflegen in die Versionsverwaltung. Im modernen Entwicklungsprozess gibt es einen Rechner, auf dem eine Software zur kontinuierlichen Integration läuft (engl. continuous integration). Diese Systeme integrieren einen Build-Server, der die Quellen automatisch aus einer Versionsverwaltung auscheckt, compiliert und dann Testfälle und weitere Metriken laufen lässt. Diese Software übernimmt dann einen Integrationstest, da hier alle Module der Software zu einer Gesamtheit zusammengebaut werden und so Probleme aufgezeigt werden, die sich vielleicht bei isolierten Tests auf den Entwicklermaschinen nicht zeigen.
22.2.2 Testen, implementieren, testen, implementieren, testen, freuen
Bisher bietet Java keine einfache Funktion, die Strings umdreht. Unser erstes JUnit-Beispiel soll daher um eine neue Klasse Strings mit einer statische Methode reverse(String) gestrickt werden.
Zu testende Klasse schreiben
Nach dem TDD-Ansatz implementieren wir eine Klasse und die Methode, sodass korrekt übersetzt werden kann, aber alles zunächst ohne Funktionalität ist. (Auf die komplette API-Dokumentation verzichtet das Beispiel.)
public class Strings {
/**
* Reverses a given String.
*/
public static String reverse( String string ) {
return null;
}
}
Der Name und der Parametertyp »fühlen« sich richtig an, und gegen diese eigene API lässt sich nun der Testfall schreiben.
JUnit-Testfall schreiben
Spontan fällt uns ein, dass ein Leer-String umgedreht natürlich auch einen Leer-String ergibt und die Zeichenkette »abc« daher umgedreht »cba« ergibt. Unser Ziel ist es, eine möglichst gute Abdeckung aller Fälle zu bekommen. Wenn wir Fallunterscheidungen im Programmcode vermuten, sollten wir versuchen, so viele Testfälle zu finden, dass alle diese Fallunterscheidungen abgelaufen werden. Interessant sind beim Eingeben immer Sonderfälle bzw. Grenzen von Wertebereichen. (Unsere Methode gibt da nicht viel her, aber wenn wir etwa eine Substring-Funktion haben, lassen sich schnell viele Methodenübergaben finden, die interessant sind.)
package com.tutego.insel.junit.util;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
class StringsTest {
@Test
void reverse_non_null_string() {
// given
String emptyString = "";
// when
String reversed = Strings.reverse( emptyString );
// then
assertEquals( "", reversed );
assertEquals( "cba", Strings.reverse( "abc" ) );
}
}
Die Klasse zeigt fünf Besonderheiten:
Die Testklasse endet mit dem Suffix Test, aber das ist nur Konvention und nicht zwingend. Ab JUnit 5 muss die Klasse nicht mehr zwingend öffentlich sein.
Die Methoden, die sich einzelne Szenarien vornehmen und die Klassen bzw. Methoden testen, tragen die Annotation @Test. Die Annotation hat in JUnit 5 keine Attribute mehr, unter JUnit 4 konnten noch eine maximale Ausführungszeit und erwartete Ausnahmen festgesetzt werden. Ab JUnit 5 müssen die Methoden nicht mehr öffentlich sein.
Unterschiedliche Autoren verwenden unterschiedliche Benennungen der Testmethoden, eine zwingende Namenskonvention gibt es nicht. Hier ist eine Notation gewählt, bei der das Präfix die zu testende Methode benennt, gefolgt vom dem, was der Test prüft, nämlich nicht-leere Strings umzudrehen. Um die Lesbarkeit zu verbessern, trennt ein Unterstrich die Segmente. Das führt zum Methodennamen reverse_non_null_string(). Eine traditionelle Namenskonvention ist, dass die Methode, die den Test enthält, mit dem Präfix »test« beginnt und mit dem Namen der Methode endet, die sie testet. Nach diesem Bauplan könnte unsere Methode testReverse() heißen.
JUnit bietet eine Reihe von assertXXX(…)-Methoden, die den erwarteten Zustand mit dem Ist-Zustand vergleichen. Gibt es Abweichungen, folgt eine Ausnahme. assertEquals(…) nimmt intern einen equals(…)-Vergleich der beiden Objekte vor. Wenn demnach Strings.reverse("") die leere Zeichenkette "" liefert, ist alles in Ordnung, und der Test wird fortgesetzt.
Der statische Import aller statischen Eigenschaften der Klasse org.junit.jupiter.api. Assertions kürzt die Schreibweise ab, sodass im Programm statt Assertions.assertEquals(…) nur assertEquals(…) geschrieben werden kann.
22.2.3 JUnit-Tests ausführen
Zum Ausführen von Testfällen gibt es unterschiedliche Möglichkeiten:
In einer Entwicklungsumgebung lassen sich die Tests leicht ausführen. Eclipse oder IntelliJ zeigt zum Beispiel die Ergebnisse in einer JUnit-View an und bietet mit einem grünen oder roten Balken direktes visuelles Feedback.
JUnit kann über den Console Launcher die Tests von der Kommandozeile ausführen.[ 281 ](Details siehe http://junit.org/junit5/docs/current/user-guide/#running-tests-console-launcher)
Eine Testausführung in der IDE ist in der Entwicklung praktisch. Doch in einer professionellen Build-Infrastruktur werden die Tests mit Maven oder Gradle angestoßen. Bei Maven ist die Testausführung Teil einer Phase im Lebenszyklus.
Eigene Implementierung überarbeiten
In unserem Beispiel wird der Testlauf fehlschlagen, weil wir keine funktionierende Implementierung der reverse(…)-Methode haben. Bisher steht im Rumpf:
public static String reverse( String string ) {
return null;
}
public static String reverse( String string ) {
return new StringBuilder( string ).reverse().toString();
}
Beim nächsten Testlauf gibt es keinen Fehler mehr.
22.2.4 assertXXX(…)-Methoden der Klasse Assertions
Assertions ist die Klasse mit diversen assertXXX(…)-Methoden, die immer dann einen AssertionFailedError auslösen, wenn ein aktueller Wert nicht so wie der gewünschte war. Der JUnit-Runner fängt alle AssertionFailedErrors ab und speichert sie für die Statistik. Bis auf drei Ausnahmen beginnen alle Methoden der Klasse Assertions mit dem Präfix assert – zwei andere heißen fail(…), und eine heißt isArray(…). Die assertXXX(…)-Methoden gibt es einmal mit einer Testmeldung, die dann erscheint, wenn JUnit extra eine Meldung angeben soll, und einmal ohne, wenn keine Extrameldung gefragt ist. Die Meldungen können auch über einen Supplier<String> geliefert werden, das ist in der folgenden Dokumentation aber nicht mit aufgeführt.
Ist etwas wahr oder falsch?
Eigentlich reicht zum Testen die Methode assertTrue(boolean condition) aus. Ist die Bedingung wahr, so ist alles in Ordnung. Wenn nicht, gibt es intern einen AssertionFailedError, eine Unterklasse von java.lang.AssertionError, die selbst Unterklasse von java.lang.Error ist.
class org.junit.jupiter.api.Assertions
static void assertTrue(boolean condition)
static void assertTrue(String message, boolean condition)
static void assertFalse(boolean condition)
static void assertFalse(String message, boolean condition)
Außerdem gibt es assert[True|False](…) mit einem BooleanSupplier.
Ist etwas null?
Um es Entwicklern etwas komfortabler zu machen, bietet JUnit sechs Kategorien Hilfsmethoden. Zunächst sind es assertNull(…) und assertNotNull(…), die testen, ob das Argument null bzw. nicht null ist. Ein Aufruf von assertNull(Object object) ist dann nichts anderes als assertTrue(object == null):
static void assertNotNull(String message, Object object)
static void assertNull(Object object)
static void assertNull(String message, Object object)
Sind Objekte identisch?
Die nächste Kategorie testet, ob das Objekt identisch mit einem anderen Objekt ist – es geht hier nicht um equals(…)-gleich:
static void assertNotSame(Object unexpected, Object actual)
static void assertNotSame(String message, Object unexpected, Object actual)
static void assertSame(Object expected, Object actual)
static void assertSame(String message, Object expected, Object actual)
Sind Objekte gleichwertig?
Statt eines Referenztests führen die folgenden Methoden einen equals(…)-Vergleich durch:
static void assertEquals(Object expected, Object actual)
static void assertEquals(String message, Object expected, Object actual)
Sind primitive Werte gleich?
Zum Testen von primitiven Datentypen gibt es im Grunde nur drei Methoden: einmal für den Datentyp long (alles »Kleine« wird automatisch typangepasst) und float sowie double:
static void assertEquals(long expected, long actual)
static void assertEquals(float|double expected, float|double actual,
float|double delta)static void assertEquals(String message, long expected, long actual)
static void assertEquals(String message, float|double expected,
float|double actual, float|double delta)
Bei dem Vergleich von Fließkommazahlen muss bei assertEquals(…) ein Delta-Wert mitgegeben werden, in dem sich das Ergebnis bewegen muss. Das trägt der Tatsache Rechnung, dass vielleicht in der Bildschirmausgabe zwei Zahlen gleich aussehen, jedoch nicht bitweise gleich sind, wenn sich etwa kleine Rechenfehler akkumuliert haben. Sind jedoch die Fließkommazahlen in einem Wrapper, also etwa Double, verpackt, leitet ja assertEquals(…) den Test nur an die equals(…)-Methode der Wrapper-Klasse weiter, die natürlich kein Delta berücksichtigt.
Sind Arrays gleich?
Weitere Methoden vergleichen Array-Inhalte; BCSIL steht stellvertretend für byte, char, short, int, long:
static void assertArrayEquals(BCSIL[] expecteds, byte[] actuals)
static void assertArrayEquals(String message, byte[] expecteds, BCSIL[] actuals)
static void assertArrayEquals(String message, long[] expecteds, long[] actuals)
static void assertArrayEquals(String message, Object[] expecteds, Object[] actuals)
Neben den assertEquals(…)-Methoden gibt es für einige Varianten Negationen:
static void assertNotEquals(long unexpected, long actual)
static void assertNotEquals(float unexpected, float actual, float delta)
static void assertNotEquals(double unexpected, double actual, double delta)
static void assertNotEquals(Object unexpected, Object actual)
static void assertNotEquals(String message, long unexpected, long actual)
static void assertNotEquals(String message, float unexpected, float actual, float delta)
static void assertNotEquals(String message, double unexpected, double actual,
double delta)static void assertNotEquals(String message, Object unexpected, Object actual)
Ist das alles wahr oder falsch?
In JUnit 5 ist ein neuer Typ org.junit.jupiter.api.function.Executable eingezogen, mit dem sich ein beliebiger Block Code ausdrücken lässt. Assertions nimmt diesen Typ an über:
static void assertAll(Executable... executables)
static void assertAll(Stream<Executable> executables)
static void assertAll(String heading, Executable... executables)
static void assertAll(String heading, Stream<Executable> executables)
Ein Executable ist eine funktionale Schnittstelle mit einer Methode void execute() throws Throwable. Das Praktische bei assertAll(…) ist, dass sie alle Blöcke ausführt, selbst wenn es bei einem einen Fehler gibt – normalerweise bricht eine @Test-Methode dann ab. Eine Ausnahme beendet nur das aktuelle execute() – Assertions.assertAll(…) fängt diese Ausnahme auf, meldet den Fehler bei der Abarbeitung des Testcodes, macht aber sonst weiter.
22.2.5 Exceptions testen
Während der Implementierung fallen oft Dinge auf, die die eigentliche Implementierung noch nicht berücksichtigt. Dann sollte sofort diese neu gewonnene Erkenntnis in den Testfall einfließen. In unserem Beispiel soll das bedeuten, dass bisher nicht wirklich geklärt ist, was bei einem null-Argument passieren soll. Bisher gibt es eine NullPointerException, und das ist auch völlig in Ordnung, aber in einem Testfall steht das bisher nicht, dass auch wirklich eine NullPointerException folgt. Diese Fragestellung legt den Fokus auf eine gern vergessene Seite des Testens, denn Testautoren dürfen sich nicht nur darauf konzentrieren, was die Implementierung denn so alles richtig machen soll – der Test muss auch kontrollieren, ob im Fehlerfall auch dieser korrekt gemeldet wird. Wenn es nicht in der Spezifikation steht, dürfen auf keinen Fall falsche Werte geradegebügelt werden: Falsche Werte müssen immer zu einer Ausnahme oder zu einem wohldefinierten Verhalten führen.
Wir wollen unser Beispiel so erweitern, dass reverse(null) eine IllegalArgumentException auslöst. Auf zwei Arten lässt sich testen, ob die erwartete IllegalArgumentException auch wirklich kommt.
Versuch und fail(…)
Die erste Variante:
@Test void reverse_null_string_1() {
try {
Strings.reverse( null );
fail( "reverse(null) should throw IllegalArgumentException" );
}
catch ( IllegalArgumentException e ) { /* Ignore */ }
}
Führt reverse(null) zur Ausnahme, was ja gewollt ist, dann wird der catch-Block die IllegalArgumentException einfach auffangen und ignorieren, und dann geht es in der Testfunktion mit anderen Dingen weiter. Sollte keine Ausnahme folgen, so wird die Anweisung nach dem reverse(…)-Aufruf ausgeführt, und die ist fail(…). Diese Methode löst eine JUnit-Ausnahme mit einer Meldung aus und signalisiert dadurch, dass im Test etwas nicht stimmte.
assertThrows(…)
Eine zweite Möglichkeit bietet JUnit mit assertThrows(…):
@Test void reverse_null_string_2() {
assertThrows( IllegalArgumentException.class, () -> {
Strings.reverse( null );
} );
}
Insgesamt gibt es in der Klasse Assertions die folgenden Methoden zum Prüfen von Ausnahmen:
static <T extends Throwable> T assertThrows(Class<T> expectedType,
Executable executable)static <T extends Throwable> T assertThrows(Class<T> expectedType,
Executable executable, String message)static <T extends Throwable> T assertThrows(Class<T> expectedType,
Executable executable, Supplier<String> messageSupplier)
22.2.6 Grenzen für Ausführungszeiten festlegen
Nach großen Refactorings kann die Software funktional ihre Tests bestehen, ist aber vielleicht viel langsamer geworden. Dann stellt sich die Frage, ob es im Sinne des Anforderungskatalogs noch korrekt ist, wenn ein performantes Programm nach einer Änderung wie eine Schnecke läuft.
Um Laufzeitveränderungen als Gültigkeitskriterium einzuführen, kann der Test in ein assertTimeout(…) bzw. assertTimeoutPreemptively(…) gesetzt werden. Beide Methoden erwarten nach einer gegebenen Duration ein Executable oder ThrowingSupplier.
@Test
void reverse_execution_time_below_1ms() {
assertTimeout( Duration.ofMillis(1), () -> {
Strings.reverse( "abc" );
} );
}
Wird die Testmethode dann nicht innerhalb der Schranke ausgeführt, gilt das als fehlgeschlagener Test, und JUnit meldet einen Fehler.
22.2.7 Beschriftungen mit @DisplayName
Wir haben einen Methodennamen zum Beispiel reverse_non_null_string genannt, damit später bei der Testausführung der Name besser zu lesen ist. Tests können aber für die Ausgabe mit @DisplayName eine eigene Beschriftung bekommen:
@Test
@DisplayName( "reverse a non null string" )
void reverseNonNullString() { … }
So lässt sich, wenn gewünscht, ein Methodenname nach der üblichen Namenskonvention einsetzen.
22.2.8 Verschachtelte Tests
Wählen Programmierer den Ansatz, für jede zu testende Methode nur genau eine @Test-Methode zu schreiben, führt das zu einer großen Anzahl von assertXXX(…)-Methoden für unterschiedliche Bereiche. Das ist unübersichtlich und nicht optimal, da bei einem Fehler die ganze Testmethode abbricht.
Eine gute Lösung, um Tests zu einem Vorgang oder einer zu testenden Methode zu bündeln, sind verschachtelte Tests. JUnit setzt sie mit geschachtelten Klassen um, die mit @Nested annotiert sind. Die Annotation @DisplayName ist nicht zwingend, aber sehr praktisch:
class StringsTest {
@DisplayName( "reverse(string)" )
@Nested class reverse {
@Test void reverse_non_null_string() { … }
@Test void reverse_null_string_2() { … }
}
}
Sollte es in der Utility-Klasse neben reverse(String) noch weitere Methoden geben, so könnten jeweils dafür geschachtelte Klassen existieren. Prinzipiell könnten die Ebenen beliebig tief gehen.
22.2.9 Tests ignorieren
Durch Umstrukturierung von Quellcode kann es sein, dass Testcode nicht länger gültig ist und entfernt oder umgebaut werden muss. Damit der Testfall nicht ausgeführt wird, muss er nicht auskommentiert werden (das bringt den Nachteil mit sich, dass sich das Refactoring etwa im Zuge einer Umbenennung von Bezeichnern nicht auf auskommentierte Bereiche auswirkt). Stattdessen lässt sich eine weitere Annotation @Disabled an die Methode setzen:
@Disabled @Test
void reverse_non_null_string()
22.2.10 Mit Methoden der Assumptions-Klasse Tests abbrechen
Während die assertXXX(…)-Methoden bei einem Fehlschlag intern zu einer Ausnahme führen und so anzeigen, dass der Test etwas gefunden hat, was nicht korrekt ist, bietet JUnit mit Assumptions.assumeXXX(…)-Methoden die Möglichkeit, die Tests nicht fortzuführen. Das ist zum Beispiel dann sinnvoll, wenn die Testausführung nicht möglich ist, etwa weil der Testrechner keine Grafikkarte hat, das Netzwerk nicht reagiert oder das Datensystem voll ist. Dabei geht es nicht darum, zu testen, wie sich die Routine bei einem nicht vorhandenen Netzwerk verhält – das gilt es natürlich auch zu testen. Aber steht das Netzwerk nicht, dann können logischerweise auch keine Tests laufen, die das Netzwerk zwingend benötigen. Zwei der über 10 Methoden sind:
class org.junit.jupiter.api.Assumptions
static void assumeTrue(boolean assumption)
static void assumeFalse(boolean assumption)
Die assumeXXX(…)-Methoden führen zu keiner Ausnahme, brechen die Testausführung aber ab.
22.2.11 Parametrisierte Tests
Bei Testfällen werden oft die zu testen Methoden mit unterschiedlichen Werten gefüttert. Wir hatten so einen Fall:
assertEquals( "", Strings.reverse( "" ) );
assertEquals( "cba", Strings.reverse( "abc" ) );
Das riecht nach Code-Duplikation, die sich mit parametrisierten Tests reduzieren lässt.
org.junit.jupiter:junit-jupiter-params Dependency
Parametrisierte Tests sind kein Teil des JUnit-Kerns, sodass wir eine neue Abhängigkeit in der Maven-POM einfügen müssen:
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-params</artifactId>
<version>5.6.2</version>
</dependency>
@org.junit.jupiter.params.ParameterizedTest
Gehen wir zurück zum Java-Code. Für parametrisierte Tests werden die Methoden nicht mehr mit @org.junit.jupiter.api.Test annotiert, sondern stattdessen mit @org.junit.jupiter.params.ParameterizedTest.
Als Nächstes müssen gültige Eingabewerte bestimmt werden, und hierfür gibt es mehrere Möglichkeiten; zwei wollen wir uns anschauen.
Als Erstes besteht die Möglichkeit, über @ValueSource ein Sammlung von Werten vorzugeben. Diverse Datentypen sind möglich, darunter numerische Werte, Strings und Class-Objekte. JUnit läuft die Sammlung ab und übermittelt jeden Wert über den Methodenparameter an die Testmethode. Wir können dann diese Parameter verarbeiten und zum Beispiel an die zu testende Methode übergeben:
@ParameterizedTest
@ValueSource( strings = { "", " ", "abc" } )
void reverse_will_not_throw_exception_with_non_null_inputs( String input ) {
Strings.reverse( input );
}
Was wir mit @ValueSource nicht machen können, ist neben den gegebenen Werten auch die erwarteten Ergebnisse mit zu übermitteln. Das ermöglicht stattdessen @CsvSource:
@ParameterizedTest
@CsvSource( { "a,a", "ab,ba", "abc,cba" } )
void reverse_non_null_inputs( String input, String expected ) {
assertEquals( expected, Strings.reverse( input ) );
}
Achtung: Ein »,« für den leeren String können wir nicht nehmen, sonst folgt eine »java.lang.IllegalArgumentException: The string to reversed is not allowed to be null«. Standardmäßig ist der Delimiter ein Komma, aber das kann geändert werden.