Guide pour les tests dynamiques dans Junit 5

Guide des tests dynamiques dans Junit 5

1. Vue d'ensemble

Le test dynamique est un nouveau modèle de programmation introduit dans JUnit 5. Dans cet article, nous allons voir ce que sont exactement les tests dynamiques et comment les créer.

Si vous êtes complètement nouveau dans JUnit 5, vous voudrez peut-être vérifier lespreview of JUnit 5 etour primary guide.

2. Qu'est-ce qu'unDynamicTest?

Les tests standard annotés avec l'annotation@Test sont des tests statiques qui sont entièrement spécifiés au moment de la compilation. A DynamicTest is a test generated during runtime. Ces tests sont générés par une méthode d'usine annotée avec l'annotation@TestFactory.

Une méthode@TestFactory doit renvoyer des instancesStream,Collection,Iterable ouIterator deDynamicTest. Renvoyer autre chose entraînera unJUnitException puisque les types de retour non valides ne peuvent pas être détectés au moment de la compilation. En dehors de cela, une méthode@TestFactory ne peut pas être static ouprivate.

LesDynamicTests sont exécutés différemment des@Tests standard et ne prennent pas en charge les rappels de cycle de vie. Cela signifie que les@BeforeEach and the @AfterEach methods will not be called for the DynamicTests.

3. Création deDynamicTests

Tout d'abord, examinons différentes manières de créer desDynamicTests.

Les exemples ici ne sont pas de nature dynamique, mais ils fourniront un bon point de départ pour créer des exemples vraiment dynamiques.

Nous allons créer unCollection deDynamicTest:

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

La méthode@TestFactory indique à JUnit qu'il s'agit d'une usine pour créer des tests dynamiques. Comme nous pouvons le voir, nous ne renvoyons qu'unCollection deDynamicTest. Each of the DynamicTest consists of two parts, the name of the test or the display name, and an Executable.

La sortie contiendra le nom complet que nous avons transmis aux tests dynamiques:

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

Le même test peut être modifié pour renvoyer unIterable,Iterator ou unStream:

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

Veuillez noter que si le@TestFactory renvoie unStream, alors il sera automatiquement fermé une fois tous les tests exécutés.

La sortie sera à peu près la même que celle du premier exemple. Il contiendra le nom d'affichage que nous passons au test dynamique.

4. Création d'unStream deDynamicTests

À des fins de démonstration, considérons unDomainNameResolver qui renvoie une adresse IP lorsque nous transmettons le nom de domaine en entrée.

Par souci de simplicité, examinons le squelette de haut niveau de notre méthode d'usine:

@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...*/
}

Il n'y a pas beaucoup de code lié àDynamicTest ici à part l'annotation@TestFactory, que nous connaissons déjà.

Les deuxArrayLists seront utilisés respectivement comme entrée pourDomainNameResolver et comme sortie attendue.

Jetons maintenant un coup d'œil au générateur d'entrée:

Iterator inputGenerator = inputList.iterator();

Le générateur d'entrée n'est rien d'autre qu'unIterator deString. Il utilise nosinputList et renvoie le nom de domaine un par un.

Le générateur de noms d'affichage est assez simple:

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

La tâche d'un générateur de nom d'affichage consiste simplement à fournir un nom d'affichage pour le scénario de test qui sera utilisé dans les rapports JUnit ou dans l'onglet JUnit de notre IDE.

Ici, nous utilisons simplement le nom de domaine pour générer des noms uniques pour chaque test. Il n’est pas nécessaire de créer des noms uniques, mais cela vous aidera en cas d’échec. Ayant cela, nous pourrons indiquer le nom de domaine pour lequel le scénario de test a échoué.

Voyons maintenant la partie centrale de notre test - le code d'exécution du test:

DomainNameResolver resolver = new DomainNameResolver();
ThrowingConsumer testExecutor = (input) -> {
    int id = inputList.indexOf(input);

    assertEquals(outputList.get(id), resolver.resolveDomain(input));
};

Nous avons utilisé leThrowingConsumer, qui est un@FunctionalInterface pour écrire le cas de test. Pour chaque entrée générée par le générateur de données, nous récupérons la sortie attendue desoutputList et la sortie réelle d'une instance deDomainNameResolver.

Maintenant, la dernière partie consiste simplement à assembler toutes les pièces et à retourner sous forme deStream deDynamicTest:

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

C'est ça. L'exécution du test affichera le rapport contenant les noms définis par notre générateur de noms d'affichage:

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

5. Amélioration desDynamicTest à l'aide des fonctionnalités de Java 8

La fabrique de tests décrite dans la section précédente peut être considérablement améliorée en utilisant les fonctionnalités de Java 8. Le code résultant sera beaucoup plus propre et pourra être écrit en un nombre inférieur de lignes:

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

Le code ci-dessus a le même effet que celui de la section précédente. LeinputList.stream().map() fournit le flux d'entrées (générateur d'entrées). Le premier argument dedynamicTest() est notre générateur de nom d'affichage («Resolving:» +dom) tandis que le second argument, alambda, est notre exécuteur de test.

La sortie sera la même que celle de la section précédente.

6. Exemple supplémentaire

Dans cet exemple, nous explorons plus en détail la puissance des tests dynamiques pour filtrer les entrées en fonction des cas de test:

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

La méthodesave(Long) n'a besoin que desemployeeId. Par conséquent, il utilise toutes les instancesEmployee. La méthodesave(Long, String) nécessitefirstName en dehors desemployeeId. Par conséquent, il filtre les instancesEmployee sansfirstName.

Enfin, nous combinons les deux flux et renvoyons tous les tests sous la forme d'un seulStream.

Voyons maintenant le résultat:

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

Les tests paramétrés peuvent remplacer la plupart des exemples de cet article. However, the dynamic tests differ from the parameterized tests as they support full test lifecycle, while parametrized tests don’t.

De plus, les tests dynamiques offrent plus de flexibilité quant à la manière dont l'entrée est générée et à la manière dont les tests sont exécutés.

JUnit 5 prefers extensions over features principle. Par conséquent, l'objectif principal des tests dynamiques est de fournir un point d'extension pour les frameworks ou extensions tiers.

Vous pouvez en savoir plus sur les autres fonctionnalités de JUnit 5 dans nosarticle on repeated tests in JUnit 5.

N'oubliez pas de consulter le code source complet de cearticle on GitHub.