Anleitung zu dynamischen Tests in Junit 5

Leitfaden für dynamische Tests in Junit 5

1. Überblick

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

Wenn Sie mit JUnit 5 noch nicht vertraut sind, sollten Sie diepreview of JUnit 5 undour primary guide überprüfen.

2. Was ist einDynamicTest?

Die mit@Test Annotation annotierten Standardtests sind statische Tests, die zum Zeitpunkt der Kompilierung vollständig spezifiziert sind. A DynamicTest is a test generated during runtime. Diese Tests werden durch eine Factory-Methode generiert, die mit der Annotation@TestFactory versehen ist.

Eine@TestFactory-Methode mussStream,Collection,Iterable oderIterator vonDynamicTest Instanzen zurückgeben. Wenn Sie etwas anderes zurückgeben, erhalten SieJUnitException, da die ungültigen Rückgabetypen beim Kompilieren nicht erkannt werden können. Abgesehen davon kann eine@TestFactory-Methode nicht static oderprivate sein.

DieDynamicTests werden anders ausgeführt als die Standard@Tests und unterstützen keine Lebenszyklus-Rückrufe. Das heißt, die@BeforeEach and the @AfterEach methods will not be called for the DynamicTests.

3. DynamicTests erstellen

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

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

Wir werdenCollection vonDynamicTest 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 Methode@TestFactory teilt JUnit mit, dass dies eine Factory zum Erstellen dynamischer Tests ist. Wie wir sehen können, geben wir nurCollection vonDynamicTest zurück. Each of the DynamicTest consists of two parts, the name of the test or the display name, and an Executable.

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 einIterable,Iterator oder einStream 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@TestFactoryStream zurückgibt, es automatisch geschlossen wird, sobald alle Tests ausgeführt wurden.

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

4. Erstellen einesStream vonDynamicTests

Betrachten Sie zu Demonstrationszwecken einDomainNameResolver, das eine IP-Adresse zurückgibt, wenn wir den Domänennamen 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 von@TestFactory, mit der wir bereits vertraut sind, gibt es hier nicht viel Code fürDynamicTest.

Die zweiArrayLists werden als Eingabe fürDomainNameResolver bzw. erwartete Ausgabe verwendet.

Schauen wir uns nun den Eingangsgenerator an:

Iterator inputGenerator = inputList.iterator();

Der Eingangsgenerator ist nichts anderes als einIterator vonString. Es verwendet unsereinputList und gibt den Domainnamen einzeln 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 anzugeben, 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, dies hilft jedoch bei Fehlern. 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 habenThrowingConsumer verwendet, dh@FunctionalInterface, um den Testfall zu schreiben. Für jede vom Datengenerator generierte Eingabe rufen wir die erwartete Ausgabe vonoutputList und die tatsächliche Ausgabe von einer Instanz vonDomainNameResolver ab.

Jetzt besteht der letzte Teil einfach darin, alle Teile zusammenzusetzen und alsStream vonDynamicTest zurückzugeben:

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 derDynamicTest mithilfe von Java 8-Funktionen

Die im vorherigen Abschnitt beschriebene Testfactory kann mithilfe der Funktionen von Java 8 drastisch verbessert werden. Der resultierende Code wird viel sauberer und kann in einer geringeren Anzahl von 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, den wir im vorherigen Abschnitt gesehen haben. DasinputList.stream().map() liefert den Strom von Eingaben (Eingangsgenerator). Das erste Argument fürdynamicTest() ist unser Anzeigenamengenerator ("Resolving:" +dom), während das zweite Argument, alambda, unser Testausführender ist.

Die Ausgabe entspricht der des vorherigen Abschnitts.

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);
}

Die Methodesave(Long) benötigt nuremployeeId. Daher werden alleEmployee-Instanzen verwendet. Die Methodesave(Long, String) benötigtfirstName nebenemployeeId. Daher werden dieEmployee-Instanzen ohnefirstName. herausgefiltert

Schließlich kombinieren wir beide Streams und geben alle Tests als einzelneStream zurück.

Schauen wir uns nun die Ausgabe an:

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. Fazit

Die parametrisierten Tests können viele Beispiele in diesem Artikel ersetzen. 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 Generierung der Eingabe und der Ausführung der Tests.

JUnit 5 prefers extensions over features principle. 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 unserenarticle on repeated tests in JUnit 5.

Vergessen Sie nicht, den vollständigen Quellcode dieserarticle on GitHub zu lesen.