Nachdem wir vor einigen Tagen hier im Blog schon einen ersten Überblick über die neue Architektur von JUnit 5 sowie die wesentlichen Features der neuen Test-Engine Jupiter gegeben haben, möchten wir in diesem Artikel das Thema Parametrisierte Tests vertiefen. Schauen wir uns zuerst einmal an, welche Möglichkeiten wir dafür bisher hatten:
1@RunWith(Parameterized.class)
2public class FibonacciTest {
3
4 @Parameters public static Collection<Object[]> data() {
5 return Arrays.asList(new Object[][] {
6 {0,0},{1,1},{2,1},{3,2} })};
7
8 private int input, expected;
9
10 public FibonacciTest(int input, int expected) {
11 this.input = input; this.expected = expected;
12 }
13
14 @Test
15 public void test() {
16 assertEquals(expected, Fibonacci.compute(input));
17 }
18}
JUnit 4 beinhaltete einen Test-Runner namens Parameterized
, der dafür sorgte, dass die aus einer mit @Parameters
annotierten Methode definierten Parameter beim Erstellen einer Test-Instanz an den Konstruktor übergeben wurden. Auf diese Parameter konnte dann in Tests zugegriffen werden. Am Beispiel oben kann man gut erkennen, dass hierbei schnell ziemlich unübersichtliche und schwer wartbare Test-Konstrukte entstehen. Darüber hinaus ist mit diesem Ansatz meist nicht sinnvoll pro Testklasse mehr als eine Testmethode zu umzusetzen.
Andere Alternativen wie JUnitParams vereinfachten den Einsatz von parametrisierten Tests etwas, indem die Definition von Parametern näher am jeweiligen Test selbst erfolgte. Aber auch damit war es nicht immer einfach, den Überblick zu behalten.
1@RunWith(JUnitParamsRunner.class)
2public class PersonTest {
3
4 @Test
5 @Parameters({"0, 0", "1, 1", "2, 1", "3, 2" })
6 public void personIsAdult(int input, int expected) {
7 assertEquals(expected, Fibonacci.compute(input));
8 }
9
10}
Beide Ansätze basieren zudem auf der Verwendung von Test-Runnern. Da wir pro Testklasse aber jeweils nur einen Runner verwenden konnten war es uns nicht möglich, gleichzeitig Funktionalitäten aus anderen Test-Runnern zu benutzen. Möchten wir beispielweise den HierarchicalContextRunner einsetzen um unsere parametrisierten Tests hierarchisch zu strukturieren, so standen wir vor einem Problem.
Parametrisierte Tests mit JUnit 5
Mit Jupiter lassen sich parametrisierte Tests auf verschiedene Arten umsetzen. Es gibt einerseits die dynamischen Tests, die schon relativ früh zur Verfügung standen, es gibt aber auch die tatsächlich so genannten parametrisierten Tests, die erst mit dem Milestone 4 zu Jupiter hinzugefügt wurden.
Dynamische Tests
Im Normalfall spezifizieren wir unsere Testfälle statisch. Das bedeutet, wir implementieren eine Testmethode und kennzeichnen sie mit der Annotation @Test
. JUnit findet dann diese Tests und führt sie aus. Dynamische Tests funktionieren anders.
1class DynamicTests {
2
3 @TestFactory
4 List<DynamicTest> createDynamicTests() {
5
6 return Arrays.asList(
7 DynamicTest.dynamicTest("First dynamically created test",
8 () -> assertTrue(true)),
9
10 DynamicTest.dynamicTest("Second dynamically created test",
11 () -> assertTrue(true))
12 );
13 }
14}
Im Gegensatz zu statischen Tests implementieren wir eine Methode, die eine Collection oder einen Stream vom Typ DynamicTest
zurückliefert. An dieser Methode verwenden wir die Annotation @TestFactory
. Die dynamischen Tests selbst werden mithilfe der statischen Methode dynamicTest()
erzeugt, die als Parameter einen Anzeigenamen (äquivalent zu @DisplayName
) und den als Lambda angegebenen auszuführenden Testcode enthält. Neben DynamicTest
s kann eine Test-Factory übrigens auch DynamicContainer
als dynamisches Äquivalent zu einer statischen Testklasse zurückgegeben.
Eine Einsatzmöglichkeit für dynamische Tests ist beispielsweise, bestimmte Tests gar nicht erst zu generieren, wenn Vorbedingungen für diese nicht erfüllt sind. In statischen Tests lässt sich dasselbe Verhalten zwar über Assumptions oder durch die Verwendung der entsprechenden Extension Points erreichen – damit würden wir aber lediglich auf die Ausführung der bereits vorhandenen Tests Einfluss nehmen.
Wir können dynamische Tests aber natürlich auch verwenden, um auf Grundlage einer Datenquelle Tests zu generieren. Dies könnte immer dann sinnvoll sein, wenn eine aufwändigere Logik bei der Ermittlung der Testparameter zum Einsatz kommt (z.B. Download einer Datei der Fachabteilung, Transformation der enthaltenen Daten und anschließende Generierung der Tests).
Parametrisierte Test
Einfacher lassen sich parametrisierte Tests aber mit der Annotation @ParameterizedTest
umsetzen. Wir verwenden diese anstelle der @Test
Annotation und definieren die Quelle der zu verwendenden Parameter über eine weitere Annotation, die einen sogenannten ArgumentsProvider
anbindet:
1class ParameterizedTests {
2
3 @ParameterizedTest
4 @ValueSource(ints = {1,2,3,4,5})
5 void valueSourceTest(int param){
6 // ...
7 }
8
9}
Die von einem ArgumentsProvider
zurückgelieferten Werte müssen mit dem Typ des Methodenparameters übereinstimmen. Jupiter bringt dabei verschiedene vordefinierte ArgumentsProvider
mit den zugehörigen Annotationen mit:
Die im Beispiel verwendete @ValueSource
können wir benutzen, um über mehrere Attribute Werte unterschiedlichen Typs anzugeben. @CsvSource
und @CsvFileSource
kommen zum Einsatz, wenn wir CSV-Daten zur Grundlage unserer Tests machen möchten. Mit @EnumSource
können wir einzelne Werte aus einer Enumeration an eine Testmethode übergeben und @MethodSource
ermöglicht die Anbindung einer (im Normalfall) statischen Methode der jeweiligen Testklasse.
Eigene ArgumentsProvider verwenden
Das Konzept der parametrisieren Tests ähnelt in Grundzügen dem von JUnitParams. Es ist aber schon mit den von Jupiter bereitgestellten Bordmitteln deutlich flexibler. Noch interessanter ist die Tatsache, dass wir als Entwickler auch eigene ArgumentsProvider
entwickeln können. Eine Implementierung, die JSON-Daten anbindet, sieht beispielsweise so aus:
1@ArgumentsSource(JsonArgumentsProvider.class) 2public @interface JsonSource { 3 4 String[] value(); 5 Class<?> type(); 6}
Über die Annotation @JsonSource
können wir ein Array von String-Werten sowie den zu deserialiserenden Typ angeben. Der über @ArgumentsSource
angebundene Provider sieht dann so aus:
1public class JsonArgumentsProvider implements ArgumentsProvider, AnnotationConsumer<JsonSource> {
2
3 private String[] values;
4 private Class<?> type;
5
6 @Override
7 public void accept(final JsonSource annotation) {
8 values = annotation.value();
9 type = annotation.type();
10 }
11
12 @Override
13 public Stream<? extends Arguments> provideArguments(final ExtensionContext context) throws Exception {
14 return Arrays.stream(values)
15 .map(value -> new Gson().fromJson(value, type))
16 .map(Arguments::of);
17 }
18}
Die über das Interface @AnnotationConsumer
implementierte Methode accept()
benutzen wir, um auf die Attributwerte der Annotation @JsonSource
zuzugreifen und in der Provider-Instanz abzuspeichern. Die für das Interface ArgumentsProvider
implementierte Methode provideArguments()
sorgt dann dafür, die einzelnen value
Werte mithilfe der Bibliothek Gson in den erwarteten Typ zu deserialisieren. In unseren Tests können wir nun wie gewohnt die neue Annotation verwenden:
1@ParameterizedTest
2@JsonSource(value = "{firstname:'Jane', lastname: 'Doe'}", type = Person.class)
3void jsonSourceTest(Person param) {
4 System.out.println(param);
5}
Fazit
Viele Projekte können erfahrungsgemäß von parametrisierten Tests profitieren. Unter JUnit 4 waren diese bislang unflexibel und schwer zu handhaben. Mit der neuen Testengine Jupiter bieten sich uns viele neue Möglichkeiten. In den meisten Fällen dürfte der Ansatz mit @ParameterizedTest
die einfachere Alternative sein und dank des vollständig überarbeiteten Erweiterungskonzepts von Jupiter können wir damit nun auch spezielle Anwendungsfälle wie verschachtelte, parametrisierte Tests abbilden. Für komplexere Szenarien lassen sich gegebenenfalls auch die dynamischen Tests sinnvoll einsetzen.
Die Quellcode-Beispiele aus diesem Artikel stehen auf Github zur Verfügung, für weiterführende Informationen zu JUnit 5 und die neue Testengine Jupiter empfehlen wir die umfangreiche offizielle Dokumentation des Projekts . JUnit 5 ist gerade erst neu erschienen und wir sind gespannt, welche Erfahrungen wir in den nächsten Monaten in den Projekten damit sammeln.
Weitere Beiträge
von Reinhard Prechtl
Dein Job bei codecentric?
Jobs
Agile Developer und Consultant (w/d/m)
Alle Standorte
Weitere Artikel in diesem Themenbereich
Entdecke spannende weiterführende Themen und lass dich von der codecentric Welt inspirieren.
Gemeinsam bessere Projekte umsetzen.
Wir helfen deinem Unternehmen.
Du stehst vor einer großen IT-Herausforderung? Wir sorgen für eine maßgeschneiderte Unterstützung. Informiere dich jetzt.
Hilf uns, noch besser zu werden.
Wir sind immer auf der Suche nach neuen Talenten. Auch für dich ist die passende Stelle dabei.
Blog-Autor*in
Reinhard Prechtl
Senior IT Consultant
Du hast noch Fragen zu diesem Thema? Dann sprich mich einfach an.
Du hast noch Fragen zu diesem Thema? Dann sprich mich einfach an.