Leitfaden für dynamische Tests in Junit 5

1. Übersicht

Dynamisches Testen ist ein neues Programmiermodell, das in JUnit 5 eingeführt wurde. In diesem Artikel werden wir uns ansehen, was genau dynamische Tests sind und wie sie erstellt werden.

Wenn Sie mit JUnit 5 noch nicht vertraut sind, können Sie die Vorschau von JUnit 5 und unseren Leitfaden überprüfen.

2. Was ist ein DynamicTest ?

Die mit @ Test Annotation kommentierten Standardtests sind statische Tests, die zum Zeitpunkt der Kompilierung vollständig angegeben werden. Ein DynamicTest ist ein Test, der zur Laufzeit generiert wird . Diese Tests werden von einer Factory-Methode generiert, die mit der Annotation @TestFactory versehen ist .

Eine @ TestFactory- Methode muss einen Stream , eine Sammlung , eine Iterable oder einen Iterator von DynamicTest- Instanzen zurückgeben. Wenn Sie etwas anderes zurückgeben, wird eine JUnitException ausgelöst, da die ungültigen Rückgabetypen beim Kompilieren nicht erkannt werden können. Abgesehen davon, eine @TestFactory Methode kann nicht stati seine c oder privat .

Die DynamicTests werden anders ausgeführt als die Standard- @Tests und unterstützen keine Lebenszyklus-Rückrufe. Bedeutung, die @BeforeEach und die @AfterEach werden Methoden nicht für die aufgerufen werden dynamic s .

3. DynamicTests erstellen

Schauen wir uns zunächst verschiedene Möglichkeiten zum Erstellen von DynamicTests an .

Die Beispiele hier sind nicht dynamischer Natur, bieten jedoch einen guten Ausgangspunkt für die Erstellung wirklich dynamischer Beispiele.

Wir werden eine Sammlung von DynamicTest erstellen :

@TestFactory Collection dynamicTestsWithCollection() { return Arrays.asList( DynamicTest.dynamicTest("Add test", () -> assertEquals(2, Math.addExact(1, 1))), DynamicTest.dynamicTest("Multiply Test", () -> assertEquals(4, Math.multiplyExact(2, 2)))); }

Die @ TestFactory- Methode teilt JUnit mit, dass dies eine Factory zum Erstellen dynamischer Tests ist. Wie wir sehen können, geben wir nur eine Sammlung von DynamicTest zurück . Jeder DynamicTest besteht aus zwei Teilen, dem Namen des Tests oder dem Anzeigenamen und einer ausführbaren Datei .

Die Ausgabe enthält den Anzeigenamen, den wir an die dynamischen Tests übergeben haben:

Add test(dynamicTestsWithCollection()) Multiply Test(dynamicTestsWithCollection())

Der gleiche Test kann geändert werden, um eine Iterable , einen Iterator oder einen Stream zurückzugeben :

@TestFactory Iterable dynamicTestsWithIterable() { return Arrays.asList( DynamicTest.dynamicTest("Add test", () -> assertEquals(2, Math.addExact(1, 1))), DynamicTest.dynamicTest("Multiply Test", () -> assertEquals(4, Math.multiplyExact(2, 2)))); } @TestFactory Iterator dynamicTestsWithIterator() { return Arrays.asList( DynamicTest.dynamicTest("Add test", () -> assertEquals(2, Math.addExact(1, 1))), DynamicTest.dynamicTest("Multiply Test", () -> assertEquals(4, Math.multiplyExact(2, 2)))) .iterator(); } @TestFactory Stream dynamicTestsFromIntStream() { return IntStream.iterate(0, n -> n + 2).limit(10) .mapToObj(n -> DynamicTest.dynamicTest("test" + n, () -> assertTrue(n % 2 == 0))); }

Bitte beachten Sie, dass wenn @TestFactory einen Stream zurückgibt , dieser automatisch geschlossen wird, sobald alle Tests ausgeführt wurden.

Die Ausgabe entspricht weitgehend der des ersten Beispiels. Es enthält den Anzeigenamen, den wir an den dynamischen Test übergeben.

4. Erstellen eines Streams von DynamicTests

Betrachten Sie zu Demonstrationszwecken einen DomainNameResolver, der eine IP-Adresse zurückgibt, wenn wir den Domainnamen als Eingabe übergeben.

Schauen wir uns der Einfachheit halber das übergeordnete Skelett unserer Fabrikmethode an:

@TestFactory Stream dynamicTestsFromStream() { // sample input and output List inputList = Arrays.asList( "www.somedomain.com", "www.anotherdomain.com", "www.yetanotherdomain.com"); List outputList = Arrays.asList( "154.174.10.56", "211.152.104.132", "178.144.120.156"); // input generator that generates inputs using inputList /*...code here...*/ // a display name generator that creates a // different name based on the input /*...code here...*/ // the test executor, which actually has the // logic to execute the test case /*...code here...*/ // combine everything and return a Stream of DynamicTest /*...code here...*/ }

Abgesehen von der Annotation @TestFactory , mit der wir bereits vertraut sind, gibt es hier nicht viel Code zu DynamicTest .

Die beiden ArrayList s werden als Eingabe für DomainNameResolver bzw. als erwartete Ausgabe verwendet.

Schauen wir uns nun den Eingangsgenerator an:

Iterator inputGenerator = inputList.iterator();

Der Eingangsgenerator ist nichts anderes als ein Iterator von String . Es verwendet unsere Eingabeliste und gibt den Domainnamen nacheinander zurück.

Der Anzeigenamengenerator ist ziemlich einfach:

Function displayNameGenerator = (input) -> "Resolving: " + input;

Die Aufgabe eines Anzeigenamengenerators besteht lediglich darin, einen Anzeigenamen für den Testfall bereitzustellen, der in JUnit-Berichten oder auf der Registerkarte JUnit unserer IDE verwendet wird.

Hier verwenden wir nur den Domainnamen, um eindeutige Namen für jeden Test zu generieren. Es ist nicht erforderlich, eindeutige Namen zu erstellen, aber es hilft im Falle eines Fehlers. Auf diese Weise können wir den Domainnamen angeben, für den der Testfall fehlgeschlagen ist.

Schauen wir uns nun den zentralen Teil unseres Tests an - den Testausführungscode:

DomainNameResolver resolver = new DomainNameResolver(); ThrowingConsumer testExecutor = (input) -> { int id = inputList.indexOf(input); assertEquals(outputList.get(id), resolver.resolveDomain(input)); };

Wir haben den ThrowingConsumer verwendet , ein @FunctionalInterface zum Schreiben des Testfalls. Für jede vom Datengenerator generierte Eingabe rufen wir die erwartete Ausgabe aus der Ausgabeliste und die tatsächliche Ausgabe aus einer Instanz von DomainNameResolver ab .

Jetzt besteht der letzte Teil einfach darin, alle Teile zusammenzusetzen und als Stream von DynamicTest zurückzukehren :

return DynamicTest.stream( inputGenerator, displayNameGenerator, testExecutor);

Das ist es. Wenn Sie den Test ausführen, wird der Bericht mit den Namen angezeigt, die von unserem Anzeigenamengenerator definiert wurden:

Resolving: www.somedomain.com(dynamicTestsFromStream()) Resolving: www.anotherdomain.com(dynamicTestsFromStream()) Resolving: www.yetanotherdomain.com(dynamicTestsFromStream())

5. Verbessern des DynamicTest mithilfe von Java 8-Funktionen

Die im vorherigen Abschnitt beschriebene Testfactory kann durch die Verwendung der Funktionen von Java 8 drastisch verbessert werden. Der resultierende Code ist viel sauberer und kann in weniger Zeilen geschrieben werden:

@TestFactory Stream dynamicTestsFromStreamInJava8() { DomainNameResolver resolver = new DomainNameResolver(); List domainNames = Arrays.asList( "www.somedomain.com", "www.anotherdomain.com", "www.yetanotherdomain.com"); List outputList = Arrays.asList( "154.174.10.56", "211.152.104.132", "178.144.120.156"); return inputList.stream() .map(dom -> DynamicTest.dynamicTest("Resolving: " + dom, () -> {int id = inputList.indexOf(dom); assertEquals(outputList.get(id), resolver.resolveDomain(dom)); })); }

Der obige Code hat den gleichen Effekt wie der im vorherigen Abschnitt. Die inputList.stream (). Map () liefert den Eingangsstrom (Eingangsgenerator). Das erste Argument für dynamicTest () ist unser Anzeigenamengenerator ("Resolving:" + dom ), während das zweite Argument, ein Lambda , unser Test Executor ist.

Die Ausgabe ist dieselbe wie im vorherigen Abschnitt.

6. Zusätzliches Beispiel

In diesem Beispiel untersuchen wir die Leistungsfähigkeit der dynamischen Tests, um die Eingaben basierend auf den Testfällen zu filtern:

@TestFactory Stream dynamicTestsForEmployeeWorkflows() { List inputList = Arrays.asList( new Employee(1, "Fred"), new Employee(2), new Employee(3, "John")); EmployeeDao dao = new EmployeeDao(); Stream saveEmployeeStream = inputList.stream() .map(emp -> DynamicTest.dynamicTest( "saveEmployee: " + emp.toString(), () -> { Employee returned = dao.save(emp.getId()); assertEquals(returned.getId(), emp.getId()); } )); Stream saveEmployeeWithFirstNameStream = inputList.stream() .filter(emp -> !emp.getFirstName().isEmpty()) .map(emp -> DynamicTest.dynamicTest( "saveEmployeeWithName" + emp.toString(), () -> { Employee returned = dao.save(emp.getId(), emp.getFirstName()); assertEquals(returned.getId(), emp.getId()); assertEquals(returned.getFirstName(), emp.getFirstName()); })); return Stream.concat(saveEmployeeStream, saveEmployeeWithFirstNameStream); }

The save(Long) method needs only the employeeId. Hence, it utilizes all the Employee instances. The save(Long, String) method needs firstName apart from the employeeId. Hence, it filters out the Employee instances without firstName.

Finally, we combine both the streams and return all the tests as a single Stream.

Now, let's have a look at the output:

saveEmployee: Employee [id=1, firstName=Fred](dynamicTestsForEmployeeWorkflows()) saveEmployee: Employee [id=2, firstName=](dynamicTestsForEmployeeWorkflows()) saveEmployee: Employee [id=3, firstName=John](dynamicTestsForEmployeeWorkflows()) saveEmployeeWithNameEmployee [id=1, firstName=Fred](dynamicTestsForEmployeeWorkflows()) saveEmployeeWithNameEmployee [id=3, firstName=John](dynamicTestsForEmployeeWorkflows())

7. Conclusion

The parameterized tests can replace many of the examples in this article. However, the dynamic tests differ from the parameterized tests as they support full test lifecycle, while parametrized tests don't.

Darüber hinaus bieten dynamische Tests mehr Flexibilität hinsichtlich der Art und Weise, wie die Eingabe generiert und die Tests ausgeführt werden.

JUnit 5 bevorzugt Erweiterungen gegenüber dem Funktionsprinzip. Daher besteht das Hauptziel dynamischer Tests darin, einen Erweiterungspunkt für Frameworks oder Erweiterungen von Drittanbietern bereitzustellen.

Weitere Informationen zu anderen Funktionen von JUnit 5 finden Sie in unserem Artikel über wiederholte Tests in JUnit 5.

Vergessen Sie nicht, den vollständigen Quellcode dieses Artikels auf GitHub zu lesen.