Développons en Java v 2.40 Copyright (C) 1999-2023 Jean-Michel DOUDOUX. |
|||||||
Niveau : | Supérieur |
Version utilisée : | 5.0 |
JUnit est un framework mature pour permettre l'écriture et l'exécution de tests automatisés.
JUnit 4 a été publié en 2005 pour permettre la prise en compte des annotations de Java 5.
JUnit 5, publié en 2017, utilise des fonctionnalités de Java 8 notamment les lambdas, les annotations répétées, ...
JUnit 5 est une réécriture intégrale du framework ayant plusieurs objectifs :
|
Les classes de tests JUnit 5 sont similaires à celles de JUnit 4 : basiquement, il suffit d'écrire une classe contenant des méthodes annotées avec @Test. Cependant, JUnit 5 est une réécriture complète de l'API contenue dans des packages différents de ceux de JUnit 4.
JUnit 5 apporte cependant aussi son lot de nouvelles fonctionnalités :
Contrairement aux versions précédentes livrées en un seul jar, JUnit 5 est livré sous la forme de différents modules notamment pour répondre à la nouvelle architecture qui sépare :
JUnit 5 ne peut être utilisée qu'avec une version supérieure ou égale à 8 de Java : il n'est pas possible d'utiliser une version antérieure.
Ce chapitre contient plusieurs sections :
La version 5 de JUnit est composée de trois sous-projets :
L'objectif de cette architecture est de séparer les responsabilités des tests, d'exécution et d'extensions. Elle doit aussi permettre de faciliter l'intégration d'autres frameworks de tests dans JUnit.
JUnit 5 utilise des fonctionnalités de Java 8 donc pour l'utiliser, il est nécessaire d'avoir une version 8 ou ultérieure de Java. La version 1.0 de JUnit 5 est diffusée en septembre 2017.
JUnit 5 est livré sous la forme de plusieurs jars qu'il faut ajouter au classpath en fonction des besoins. Le plus simple est d'utiliser Maven.
Group ID |
Version |
Artefact ID |
org.unit.jupiter |
5.0.0 |
junit-jupiter-api junit-jupiter-engine junit-jupiter-params |
org.junit.platform |
1.0.0 |
junit-platform-commons junit-platform-console junit-platform-console-standalone junit-platform-engine junit-platform-gradle-plugin junit-platform-launcher junit-platform-runner junit-platform-suite-api junit-platform-surefire-provider |
org.junit.vintage |
4.12.0 |
junit-vintage-engine |
Les dépendances peuvent être définies dans un projet Maven selon les besoins.
Exemple : |
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<java.version>1.8</java.version>
<junit.version>4.12</junit.version>
<junit.jupiter.version>5.0.0</junit.jupiter.version>
<junit.vintage.version>${junit.version}.0</junit.vintage.version>
<junit.platform.version>1.0.0</junit.platform.version>
</properties>
<dependencies>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
<version>${junit.jupiter.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-params</artifactId>
<version>${junit.jupiter.version}</version>
<scope>test</scope>
</dependency>
<!-- Pour executer des tests ecrits avec un IDE
qui ne supporte que les versions precedentes de JUnit -->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>${junit.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.platform</groupId>
<artifactId>junit-platform-runner</artifactId>
<version>${junit.platform.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-engine</artifactId>
<version>${junit.jupiter.version}</version>
</dependency>
<dependency>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
<version>${junit.vintage.version}</version>
</dependency>
</dependencies>
L'écriture de classes de tests avec JUnit 5 est similaire à celle avec JUnit 4 : il faut définir des méthodes annotées avec des annotations de JUnit5. Certaines de ces annotations ont été renommées, notamment celles relatives au cycle de vie des instances de tests et d'autres ont été ajoutées. Parmi celles-ci, JUnit 5 propose une annotation permet de définir un nom d'affichage pour un cas de test, une autre permet de définir un test imbriqué sous la classe d'une classe interne.
Contrairement à JUnit 4, les classes et les méthodes de tests n'ont plus l'obligation d'être public. Avec JUnit 5, elles peuvent aussi être package friend (visibilité par défaut si aucune visibilité n'est précisée).
Les méthodes qui implémentent un cas de test utilisent des assertions pour effectuer des vérifications des résultats de l'exécution du test. Ces assertions ont été réécrites : certaines surcharges attendent en paramètres des interfaces fonctionnelles qui peuvent donc être définies avec des expressions Lambda. Quelques nouvelles assertions ont été ajoutées notamment une qui permet de définir un groupe d'assertions qui seront toutes évaluées ensemble.
JUnit Jupiter propose plusieurs annotations pour la définition et la configuration des tests. Ces annotations sont dans le package org.junit.jupiter.api.
Annotation |
Rôle |
@Test |
La méthode annotée est un cas de test. Contrairement à l'annotation @Test de JUnit, celle-ci ne possède aucun attribut |
@ParameterizedTest |
La méthode annotée est un cas de test paramétré |
@RepeatedTest |
La méthode annotée est un cas de test répété |
@TestFactory |
La méthode annotée est une fabrique pour des tests dynamiques |
@TestInstance |
Configurer le cycle de vie des instances de tests |
@TestTemplate |
La méthode est un modèle pour des cas de tests à exécution multiple |
@DisplayName |
Définir un libellé pour la classe ou la méthode de test annotée |
@BeforeEach |
La méthode annotée sera invoquée avant l'exécution de chaque méthode de la classe annotée avec @Test, @RepeatedTest, @ParameterizedTest ou @Testfactory. Cette annotation est équivalente à @Before de JUnit 4 |
@AfterEach |
La méthode annotée sera invoquée après l'exécution de chaque méthode de la classe annotée avec @Test, @RepeatedTest, @ParameterizedTest ou @Testfactory. Cette annotation est équivalente à @After de JUnit 4 |
@BeforeAll |
La méthode annotée sera invoquée avant l'exécution de la première méthode de la classe annotée avec @Test, @RepeatedTest, @ParameterizedTest ou @Testfactory. Cette annotation est équivalente à @BeforeClass de JUnit 4. La méthode annotée doit être static sauf si le cycle de vie de l'instance est per-class |
@AfterAll |
La méthode annotée sera invoquée après l'exécution de toutes les méthodes de la classe annotées avec @Test, @RepeatedTest, @ParameterizedTest et @Testfactory. Cette annotation est équivalente à @AfterClass de JUnit 4. La méthode annotée doit être static sauf si le cycle de vie de l'instance est per-class. |
@Nested |
Indiquer que la classe annotée correspond à un test imbriqué |
@Tag |
Définir une balise sur une classe ou une méthode qui permettra de filtrer les tests exécutés. Cette annotation est équivalente aux Categories de JUnit 4 ou aux groups de TestNG |
@Disabled |
Désactiver les tests de la classe ou la méthode annotée. Cette annotation est similaire à @Ignore de JUnit 4 |
@ExtendWith |
Enregistrer une extension |
Les méthodes annotées avec @Test, @TestTemplate, @RepeatedTest, @BeforeAll, @AfterAll, @BeforeEach ou @AfterEach ne doivent pas retourner de valeur.
Les annotations de JUnit Jupiter peuvent être utilisées comme méta-annotation : il est possible de définir des annotations, elles-mêmes annotées avec ces annotations pour qu'elles héritent de leurs caractéristiques.
Exemple ( code Java 8 ) : |
package fr.jmdoudoux.dej.junit5.TestJUnit5;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.Test;
@Target({ ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Tag("Mon_Tag")
@Test
public @interface TestAvecMonTag {
}
L'écriture de tests standard avec JUnit 5 est très similaire à celle de de JUnit 4. Basiquement, il faut écrire une classe contenant des méthodes annotées pour implémenter les cas de tests ou le cycle de vie des tests. Les méthodes qui implémentent des cas de tests utilisent des assertions pour faire les vérifications requises.
La définition d'un cas de test se fait avec l'annotation @org.junit.jupiter.api.Test utilisée sur une méthode. Cette annotation est similaire à celle de JUnit avec quelques différences :
Exemple ( code Java 8 ) : |
package fr.jmdoudoux.dej.junit5;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
public class MonTest {
@Test
public void simpleTest() {
System.out.println("simpleTest");
Assertions.assertTrue(true);
}
}
Maintenant avec JUnit 5, ni les classes ni les méthodes de tests n'ont l'obligation d'être public : ils peuvent avoir la visibilité package friend (sans marqueur de visibilité).
Exemple ( code Java 8 ) : |
package fr.jmdoudoux.dej.junit5;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
public class MonTest {
@Test
void simpleTest() {
System.out.println("simpleTest");
Assertions.assertTrue(true);
}
}
Attention : les méthodes private sont ignorées et ne sont pas exécutées.
Les classes et les méthodes de tests peuvent avoir un libellé qui sera affiché par les tests runners ou dans le rapport d'exécution des tests. Ce libellé est défini en utilisant l'annotation @org.junit.jupiter.api.DisplayName. Elle n'attend qu'un seul attribut obligatoire qui précise le libellé.
Exemple ( code Java 8 ) : |
package fr.jmdoudoux.dej.junit5.TestJUnit5;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
@DisplayName("Ma classe de test JUnit5")
public class MonTest {
@Test
@DisplayName("Mon cas de test")
void premierTest() {
// ...
}
}
Comme avec JUnit 4, par défaut JUnit 5 créé une nouvelle instance pour exécuter chaque méthode de tests.
Une classe de test JUnit peut avoir des méthodes annotées pour définir des actions exécutées durant le cycle de vie des tests. Le cycle de vie d'un test peut être enrichi grâce à quatre annotations utilisées sur des méthodes pour réaliser des initialisations ou du ménage :
A part leur nom, ces annotations fonctionnent de manière similaire à leurs équivalents JUnit 4.
Par défaut, une nouvelle instance est créée pour exécuter chaque méthode de tests : il n'y a alors pas d'instance à utiliser pour invoquer les méthodes @BeforeAll et @AfterAll. Celles-ci doivent donc être statique.
Exemple ( code Java 8 ) : |
package fr.jmdoudoux.dej.junit5;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
public class MonTest {
@BeforeAll
static void initAll() {
System.out.println("beforeAll");
}
@BeforeEach
void init() {
System.out.println("beforeEach");
}
@AfterEach
void tearDown() {
System.out.println("afterEach");
}
@AfterAll
static void tearDownAll() {
System.out.println("afterAll");
}
@Test
void simpleTest() {
System.out.println("simpleTest");
Assertions.assertTrue(true);
}
@Test
void secondTest() {
System.out.println("secondTest");
Assertions.assertTrue(true);
}
}
Résultat : |
beforeAll
beforeEach
simpleTest
afterEach
beforeEach
secondTest
afterEach
afterAll
L'ordre d'exécution de méthodes annotées avec les mêmes annotations (par exemple @BeforeAll) est indéfini.
Une méthode annotée avec @BeforeAll sera exécutée avant l'exécution de la première méthode de tests. Avec le cycle de vie par défaut des instances de tests, il est obligatoire qu'une méthode annotée avec @BeforeAll soit statique.
L'annotation @BeforeAll de JUnit 5 est équivalente à l'annotation @BeforeClass de JUnit 4.
Une méthode annotée avec @AfterAll est exécutée après l'exécution de toutes les méthodes de tests de la classe.
Avec le cycle de vie par défaut des instances de tests, il est obligatoire qu'une méthode annotée avec @AfterAll soit statique.
L'annotation @AfterAll de JUnit 5 est équivalente à l'annotation @AfterClass de JUnit 4.
Une méthode annotée avec @BeforeEach sera exécutée avant chaque exécution d'une méthode de tests. Elle ne peut pas être statique sinon une exception de type JUnitException est levée à l'exécution.
L'annotation @BeforeEach de JUnit 5 est équivalente à l'annotation @Before de JUnit 4.
Une méthode annotée avec @AfterEach sera exécutée après chaque exécution d'une méthode de tests. Elle ne peut pas être statique sinon une exception de type JUnitException est levée à l'exécution.
L'annotation @AfterEach de JUnit 5 est équivalente à l'annotation @After de JUnit 4.
Les assertions ont pour rôle de faire des vérifications pour le test en cours. Si ces vérifications échouent, alors l'assertion lève une exception qui fait échouer le test.
JUnit Jupiter contient la plupart des assertions de JUnit 4 mais propose aussi ses propres annotations dont certaines surcharges peuvent utiliser les lambdas. Ces assertions sont des méthodes statiques de la classe org.junit.jupiter.Assertions.
Les assertions classiques permettent de faire des vérifications sur une instance ou une valeur ou effectuer des comparaisons. La classe org.junit.jupiter.Assertions contient de nombreuses méthodes statiques qui permettent d'effectuer différentes vérifications de données. Ces assertions permettent de comparer les données obtenues avec celles attendues dans un cas de test.
Egalité |
Nullité |
Exceptions |
assertEquals() |
assertNull() |
assertThrows() |
assertNotEquals() |
assertNotNull() |
|
assertTrue() |
||
assertFalse() |
||
assertSame() |
||
assertNotSame() |
Les assertions classiques de JUnit 5 sont similaires à celles correspondantes de JUnit 4 :
Les assertions JUnit 5 possèdent cependant des différences par rapport à leur équivalent JUnit 4 :
Exemple ( code Java 8 ) : |
package fr.jmdoudoux.dej.junit5;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotSame;
import static org.junit.jupiter.api.Assertions.assertTrue;
import java.util.Arrays;
import java.util.LinkedList;
import java.util.List;
import org.junit.jupiter.api.Test;
public class PremiereClasseTest {
@Test
void monPremierTest() {
assertTrue(true);
assertTrue(this::isValide);
assertTrue(true, () -> "Description " + "du cas " + "de test");
List<String> attendu = Arrays.asList("e1", "e2", "e2");
List<String> actual = new LinkedList<>(attendu);
assertEquals(attendu, actual);
assertEquals(attendu, actual, "Les listes ne sont pas égales");
assertEquals(attendu, actual, () -> "Les listes " + "ne sont " + "pas égales");
assertNotSame(attendu, actual, "Les instances sont les memes");
}
boolean isValide() {
return true;
}
}
En plus des assertions classiques, de nouvelles assertions sont aussi ajoutées :
La manière de vérifier une exception attendue change en JUnit 5. Avec JUnit 4, il fallait utiliser un attribut de l'annotation @Test ou protéger le code dans un bloc try/catch. Avec JUnit 5, il suffit d'utiliser l'assertion assertThrows().
L'assertion assertAll permet de regrouper plusieurs assertions qui seront toutes exécutés. L'assertion assertAll échoue si au moins une des assertions qu'elle regroupe échoue. Même si une assertion du groupe échoue, toutes les assertions du groupe seront évaluées.
Cette fonctionnalité très pratique, notamment pour vérifier l'état d'un POJO, peut être mise en oeuvre facilement grâce à l'utilisation d'une ou plusieurs expressions Lambda.
La méthode assertAll() de la classe Assertions possède plusieurs surcharges :
La méthode assertAll() vérifie que l'exécution de tous les Executable fournis ne lève aucune exception.
Si la vérification d'au moins une des assertions définies dans le groupe échoue alors la méthode assertAll() lève une exception de type org.opentest4j.MultipleFailuresError. L'affichage de l'erreur est de la responsabilité du moteur d'exécution des tests.
Exemple ( code Java 8 ) : |
package fr.jmdoudoux.dej.junit5;
import java.awt.Dimension;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
public class DimensionTest {
@Test
void verifierAttributs() {
Dimension sut = new Dimension(800, 600);
Assertions.assertAll("Dimensions non conformes",
() -> Assertions.assertTrue(sut.getWidth() == 801, "Valeur de width erronee"),
() -> Assertions.assertTrue(sut.getHeight() == 601, "Valeur de height erronee"));
}
}
Résultat : |
org.opentest4j.MultipleFailuresError: Dimensions non conformes (2 failures)
Valeur de width erronee
Valeur de height erronee
L'assertion assertArrayEquals vérifie que deux tableaux sont égaux.
La méthode assertArrayEquals() possède de nombreuses surcharges pour des tableaux de types boolean[], byte[], char[], double[], float[], int[], long[] et short[] et éventuellement un message sous la forme d'une chaîne de caractères ou d'un Supplier<String> :
Plusieurs autres surcharges pour les tableaux de type float[] et double[] permettent de préciser une valeur qui sera utilisée comme marge lors des comparaisons des valeurs.
Les surcharges qui attendent des tableaux d'Object vérifie l'égalité de manière profonde.
Exemple ( code Java 8 ) : |
@Test
void verifierEgaliteTableaux() {
Assertions.assertArrayEquals(new int[] { 1, 2, 3 }, new int[] { 1, 2, 3 },
"Egalite des tableaux");
}
Le test ci-dessous échoue car l'ordre des éléments des deux tableaux est différent.
Exemple ( code Java 8 ) : |
@Test
void verifierEgaliteTableaux() {
Assertions.assertArrayEquals(new int[] { 1, 2, 3 }, new int[] { 3, 2, 1 },
"Egalite des tableaux");
}
Résultat : |
org.opentest4j.AssertionFailedError: Egalite
des tableaux ==> array contents differ at index [0], expected: <1> but was: <3>
Le test ci-dessous échoue car le nombre d'éléments des deux tableaux n'est pas identique.
Exemple ( code Java 8 ) : |
@Test
void verifierEgaliteTableaux() {
Assertions.assertArrayEquals(new int[] { 1, 2, 3 }, new int[] { 1, 2, 3, 4 },
"Egalite des tableaux");
}
Résultat : |
org.opentest4j.AssertionFailedError: Egalite des tableaux ==> array
lengths differ, expected: <3> but was: <4>
L'assertion assertEquals permet de vérifier que la valeur actuelle et la valeur attendue soient égales.
La méthode assertEquals() de la classe Assertions possède de nombreuses surcharges pour différents types de données et éventuellement un message sous la forme d'une chaîne de caractères ou d'un Supplier<String>.
Le type de données des surcharges supportés sont : byte, char, double, float, int long, Object et short.
Pour les types double et float les surcharges sont différentes : elles acceptent en plus une valeur de type double nommée delta qui permet préciser une valeur qui servira de marge lors de la comparaison.
Exemple ( code Java 8 ) : |
@Test
void verifierEgalite() {
Dimension sut = new Dimension(801, 601);
Assertions.assertEquals(new Dimension(800, 600), sut, "Dimensions non egales");
}
Résultat : |
org.opentest4j.AssertionFailedError: Dimensions non egales ==> expected:
<java.awt.Dimension[width=800,height=600]> but was: <java.awt.Dimension[width=801,height=601]>
L'assertion assertNotEquals permet de vérifier que la valeur actuelle et la valeur attendue ne soient pas égales.
Contrairement à la méthode assertEquals(), la méthode assertNotEquals() ne possède des surcharges que pour le type Object.
Exemple ( code Java 8 ) : |
@Test
void verifierInegalite() {
Dimension sut = new Dimension(800, 600);
Assertions.assertNotEquals(new Dimension(800, 600), sut, "Dimensions egales");
}
Résultat : |
org.opentest4j.AssertionFailedError: Dimensions egales ==> expected: not equal but was:
<java.awt.Dimension[width=800,height=600]>
L'assertion assertTrue permet de vérifier que la condition fournie est vraie.
La méthode assertTrue() de la classe Assertions possède plusieurs surcharges pour fournir la condition sous la forme d'un booléen ou d'un BooleanSupplier et éventuellement un message :
Exemple ( code Java 8 ) : |
@Test
void verifierTrue() {
boolean bool = true;
Assertions.assertTrue(bool);
Assertions.assertTrue(MonTest::getBooleen, "Booleen different de true");
}
static boolean getBooleen() {
return false;
}
Résultat : |
org.opentest4j.AssertionFailedError: Booleen different de true
L'assertion assertFalse permet de vérifier que la condition fournie est fausse.
La méthode assertFalse() de la classe Assertions possède plusieurs surcharges pour fournir la condition sous la forme d'un booléen ou d'un BooleanSupplier et éventuellement un message :
Exemple ( code Java 8 ) : |
@Test
void verifierFalse() {
boolean bool = false;
Assertions.assertFalse(bool);
Assertions.assertFalse(MonTest::getBooleen, "Booleen different de false");
}
static boolean getBooleen() {
return true;
}
}
Résultat : |
org.opentest4j.AssertionFailedError: Booleen different de false
L'assertionIterableEquals permet de vérifier que deux Iterables sont égaux de manière profonde, ce qui implique plusieurs vérifications :
La méthode assertIterableEquals() possède plusieurs surcharges qui permettent de préciser l'Iterable attendu, l'Iterable à vérifier et éventuellement un message sous la forme d'une chaîne de caractères ou d'un Supplier<String> :
Exemple ( code Java 8 ) : |
@Test
void verifierIterableEquals() {
Iterable<Integer> attendu = new ArrayList<>(Arrays.asList(1, 2, 3));
Iterable<Integer> actuel = new ArrayList<>(Arrays.asList(1, 2, 3));
Assertions.assertIterableEquals(attendu, actuel);
}
L'exemple ci-dessous échoue car le nombre d'éléments des deux collections est différent.
Exemple ( code Java 8 ) : |
@Test
void verifierIterableEquals() {
Iterable<Integer> attendu = new ArrayList<>(Arrays.asList(1, 2, 3));
Iterable<Integer> actuel = new ArrayList<>(Arrays.asList(1, 2));
Assertions.assertIterableEquals(attendu, actuel);
}
Résultat : |
org.opentest4j.AssertionFailedError: iterable lengths differ, expected: <3> but was: <2>
L'exemple ci-dessous échoue car l'ordre des éléments des deux collections est différent.
Exemple ( code Java 8 ) : |
@Test
void verifierIterableEquals() {
Iterable<Integer> attendu = new ArrayList<>(Arrays.asList(1, 2, 3));
Iterable<Integer> actuel = new ArrayList<>(Arrays.asList(3, 2, 1));
Assertions.assertIterableEquals(attendu, actuel);
}
Résultat : |
org.opentest4j.AssertionFailedError: iterable
contents differ at index [0], expected: <1> but was: <3>
L'assertion assertLinesMatch vérifie que les éléments d'une List<String> sont en correspondance avec une autre List<String>. Cette assertion est un cas spécifique de comparaison de collections.
La méthode assertLinesMatch() attend donc en paramètres deux List<String>.
static void assertLinesMatch(List<String> expectedLines, List<String> actualLines)
La correspondance est vérifiée en utilisant plusieurs règles pour chaque élément des listes :
Dans sa forme la plus simple, elle compare simplement les éléments des deux listes.
Exemple ( code Java 8 ) : |
@Test
void verifierLinesMatch() {
List<String> expectedLines = Arrays.asList("A1", "A2", "A3", "A4");
List<String> emails = Arrays.asList("A1", "A2", "A3", "A4");
Assertions.assertLinesMatch(expectedLines, emails);
}
Mais il est aussi possible d'utiliser des expressions régulières pour vérifier la valeur d'un élément.
Exemple ( code Java 8 ) : |
@Test
void verifierLinesMatch() {
List<String> expectedLines = Arrays.asList("(.*)@(.*)", "(.*)@(.*)");
List<String> emails = Arrays.asList("test@gmail.com", "jm@test.fr");
Assertions.assertLinesMatch(expectedLines, emails);
}
Il est aussi possible d'ignorer un ou plusieurs éléments durant la comparaison grâce à un marqueur d'avance rapide : ils peuvent par exemple permettre d'ignorer des éléments dont la valeur change à chaque exécution.
Un marqueur d'avance rapide commence et termine par «>>» et doit posséder au moins quatre caractères.
Exemple ( code Java 8 ) : |
@Test
void verifierLinesMatch() {
List<String> expectedLines = Arrays.asList("(.*)@(.*)", ">>>>", "(.*)@(.*)");
List<String> emails = Arrays.asList("test@gmail.com", "test", "email", "jm@test.fr");
Assertions.assertLinesMatch(expectedLines, emails);
}
Il est possible de mettre une description entre les doubles chevrons : cette description sera ignorée.
Exemple ( code Java 8 ) : |
@Test
void verifierLinesMatch() {
List<String> expectedLines =
Arrays.asList("(.*)@(.*)", ">> aller au dernier >>", "(.*)@(.*)");
List<String> emails = Arrays.asList("test@gmail.com", "test", "email","jm@test.fr");
Assertions.assertLinesMatch(expectedLines, emails);
}
Il est possible de préciser un nombre exact d'éléments à ignorer.
Exemple ( code Java 8 ) : |
@Test
void verifierLinesMatch() {
List<String> expectedLines = Arrays.asList("A1", ">> 2 >>", "A4");
List<String> emails = Arrays.asList("A1", "A2", "A3", "A4");
Assertions.assertLinesMatch(expectedLines, emails);
}
Si le nombre d'éléments à ignorer ne peut être atteint ou est insuffisant alors la méthode lève une exception.
Exemple ( code Java 8 ) : |
@Test
void verifierLinesMatch() {
List<String> expectedLines = Arrays.asList("A1", ">> 1 >>", "A4");
List<String> emails = Arrays.asList("A1", "A2", "A3", "A4");
Assertions.assertLinesMatch(expectedLines, emails);
}
Résultat : |
org.opentest4j.AssertionFailedError:
expected line #3:`A4` doesn't match ==> expected: <A1 >> 1 >> A4> but was: <A1
A2
A3
A4>
L'assertion assertNull permet de vérifier que l'objet fourni en paramètre est null.
La méthode assertNull() de la classe Assertions possède plusieurs surcharges pour fournir l'objet et éventuellement un message sous la forme d'une chaîne de caractères ou d'un Supplier<String> :
Exemple ( code Java 8 ) : |
@Test
void verifierNull() {
Object sut = new Dimension(800, 600);
Assertions.assertNull(sut);
}
Résultat : |
org.opentest4j.AssertionFailedError:
expected: <null> but was: <java.awt.Dimension[width=800,height=600]>
L'assertion assertNotNull permet de vérifier que l'objet fourni en paramètre n'est pas null.
La méthode assertNotNull() de la classe Assertions possède plusieurs surcharges pour fournir l'objet et éventuellement un message sous la forme d'une chaîne de caractères ou d'un Supplier<String> :
Exemple ( code Java 8 ) : |
@Test
void verifierNotNull() {
Object sut = null;
Assertions.assertNotNull(sut);
}
Résultat : |
org.opentest4j.AssertionFailedError: expected: not <null>
L'assertion assertSame permet de vérifier que les objets fournis en paramètre sont le même objet.
La méthode assertSame() de la classe Assertions possède plusieurs surcharges pour fournir les deux objets et éventuellement un message sous la forme d'une chaîne de caractères ou d'un Supplier<String> :
Exemple ( code Java 8 ) : |
@Test
void verifierSame() {
Object sut = new Dimension(800, 600);
Object expected = new Dimension(800, 600);
Assertions.assertSame(sut, expected);
}
Résultat : |
org.opentest4j.AssertionFailedError:
expected: java.awt.Dimension@10bdf5e5<java.awt.Dimension[width=800,height=600]>
but was: java.awt.Dimension@6e1ec318<java.awt.Dimension[width=800,height=600]>
L'assertion assertNotSame permet de vérifier que les objets fournis en paramètre ne sont pas le même objet.
La méthode assertNotSame() de la classe Assertions possède plusieurs surcharges pour fournir les deux objets et éventuellement un message sous la forme d'une chaîne de caractères ou d'un Supplier<String> :
Exemple ( code Java 8 ) : |
@Test
void verifierNotSame() {
Object sut = new Dimension(800, 600);
Object expected = sut;
Assertions.assertNotSame(sut, expected);
}
Résultat : |
org.opentest4j.AssertionFailedError:
expected: not same but was: <java.awt.Dimension[width=800,height=600]>
Contrairement à JUnit 4 qui utilisait des attributs de l'annotation @Test, la vérification de la levée d'une exception avec JUnit 5 se fait avec une assertion, ce qui rend plus homogène cette fonctionnalité.
L'assertion assertThrows vérifie que l'exécution de la méthode passée en paramètre lève l'exception précisée : si ce n'est pas le cas, elle lève une exception pour faire échouer le test.
La méthode assertThrows() possède plusieurs surcharges qui attendent en paramètres le type de l'exception qui doit être levée, une interface fonctionnelle de type Executable qui est le code à exécuter et éventuellement un message sous la forme d'une chaîne de caractères ou d'un Supplier<String>
Exemple ( code Java 8 ) : |
@Test
void verifierException() {
String valeur = null;
assertThrows(NumberFormatException.class, () -> {
Integer.valueOf(valeur);
});
}
Il est aussi possible de préciser une super-classe de l'exception attendue.
L'exemple de test ci-dessous réussi car l'exception NumberFormatException hérite de l'exception IllegalArgumentException.
Exemple ( code Java 8 ) : |
@Test
void verifierException() {
String valeur = null;
assertThrows(IllegalArgumentException.class, () -> {
Integer.valueOf(valeur);
});
}
Si aucune exception n'est levée par le par les traitements fournis ou si une exception différente est levée alors la méthode assertThrows() lève une exception qui fait échouer le test.
L'exemple de test ci-dessous échoue car aucune exception n'est levée.
Exemple ( code Java 8 ) : |
@Test
void verifierException() {
String valeur = "1";
assertThrows(NumberFormatException.class, () -> {
Integer.valueOf(valeur);
});
}
Résultat : |
org.opentest4j.AssertionFailedError:
Expected java.lang.NumberFormatException to be thrown, but nothing was thrown.
Si l'exception est levée par le code passé en paramètre, alors celle-ci est fournie en retour de l'exécution de la méthode assertThrows(). Il est alors aussi possible de faire des vérifications sur l'exception obtenue.
Exemple : |
package fr.jmdoudoux.dej.junit5;
public class MaClasse {
public void maMethode() {
throw new RuntimeException("mon message d'erreur");
}
}
Exemple ( code Java 8 ) : |
@Test
void verifierException() {
MaClasse sut = new MaClasse();
RuntimeException excep = assertThrows(RuntimeException.class, sut::maMethode);
assertAll(() -> assertEquals("message erreur", excep.getMessage()),
() -> assertNull(excep.getCause()));
}
Résultat : |
org.opentest4j.MultipleFailuresError: Multiple Failures (1 failure)
expected: <message erreur> but was: <mon message d'erreur>
Les assertions assertTimeout et assertTimeoutPreemptively vérifie que les traitements fournis en paramètre s'exécutent avant le délai précisé. La différence entre les deux est que assertTimeoutPreemptively interrompt l'exécution des traitements si le délai est dépassé.
La méthode assertTimeout() possède plusieurs surcharges qui permettent de préciser la durée maximale d'exécution (timeout), les traitements à exécuter sous la forme d'un Executable ou d'un ThrowingSupplier et éventuellement un message sous la forme d'une chaîne de caractères ou d'un Supplier<String> :
Exemple ( code Java 8 ) : |
@Test
void verifierTimeout() {
Assertions.assertTimeout(Duration.ofMillis(200), () -> {
return "";
});
Assertions.assertTimeout(Duration.ofSeconds(1), DimensionTest::traiter);
}
private static String traiter() throws InterruptedException {
Thread.sleep(2000);
return "";
}
Résultat : |
org.opentest4j.AssertionFailedError: execution exceeded timeout of 1000 ms by 1001 ms
La méthode assertTimeoutPreemptively() possède plusieurs surcharges qui permettent de préciser la durée maximale d'exécution (timeout), les traitements à exécuter sous la forme d'un Executable ou d'unThrowingSupplier et éventuellement un message sous la forme d'une chaîne de caractères ou d'un Supplier<String> :
Exemple ( code Java 8 ) : |
@Test
void verifierTimeoutPreemptively() {
Assertions.assertTimeoutPreemptively(Duration.ofMillis(200), () -> {
return "";
});
Assertions.assertTimeoutPreemptively(Duration.ofSeconds(1), MonTest::traiter);
}
Résultat : |
org.opentest4j.AssertionFailedError: execution timed out after 1000 ms
Les surcharges de la méthode fail() permettent de faire échouer le test en levant une exception de type AssertionFailedError.
Méthode |
Rôle |
static <V> V fail(String message) |
Faire échouer le test avec le message indiquant la raison de l'échec |
static <V> V fail(String message, Throwable cause) |
Faire échouer le test avec le message indiquant la raison et la cause de l'échec |
static <V> V fail(Supplier<String> messageSupplier) |
Faire échouer le test avec le message fourni par le Supplier indiquant la raison de l'échec |
static <V> V fail(Throwable cause) |
Faire échouer le test avec la cause de l'échec |
Exemple ( code Java 8 ) : |
@Test
void monTest() {
fail("la raison de l'échec du test");
}
JUnit Jupiter propose un ensemble d'assertions qui peuvent suffire pour des tests simples mais il est aussi possible d'utiliser d'autres bibliothèques d'assertions telles que :
Ces bibliothèques sont compatibles avec JUnit 5. C'est d'autant plus nécessaire que JUnit 5 a fait le choix de ne pas fournir d'implémentation de l'assertion assertThat() qui attendait en paramètre un objet de type Matcher de la bibliothèque Hamcrest. JUnit 5 préfère laisser les développeurs utiliser ces bibliothèques tierces.
Remarque : contrairement à JUnit 4, la bibliothèque Hamcrest n'est donc plus fournie en standard avec JUnit 5. Elle peut cependant être utilisée avec JUnit 5 si elle est ajoutée au classpath.
L'exemple ci-dessous utilise l'assertion assertThat de la bibliothèque Hamcrest.
Exemple ( code Java 8 ) : |
package fr.jmdoudoux.dej.junit5;
import static org.hamcrest.CoreMatchers.equalTo;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.MatcherAssert.assertThat;
import org.junit.jupiter.api.Test;
public class MaClasseTest {
@Test
void testAvecHamcrest() {
assertThat(1 + 2, is(equalTo(3)));
}
}
Les suppositions permettent de conditionner l'exécution de tout ou partie d'un cas de test. Elles peuvent interrompre un test (sans le faire échouer) si une condition est remplie ou peuvent conditionner l'exécution de certains traitements d'un test selon une condition.
Si l'évaluation d'une supposition échoue alors l'exécution du test est interrompue car elle lève une exception de type org.opentest4j.TestAbortedException. Dans ce cas, le test interrompu est considéré comme désactivé.
Un cas de test vide (qui n'a réalisé aucune assertion) est évalué comme réussi (vert).
Dans les deux cas, le test est réussi.
JUnit Jupiter propose des suppositions sous la forme de méthodes statiques de la classe org.junit.jupiter.Assumptions. La classe Assumptions possède plusieurs méthodes statiques : assumeTrue(), assumeFalse() et assumingThat() dont certaines surcharges attendent en paramètre des interfaces fonctionnelles qui permettent donc d'utiliser des expressions Lambdas.
La supposition assumeTrue permet d'exécuter la suite des traitements du test uniquement si la valeur booléenne fournie est true.
La méthode assumeTrue() possède plusieurs surcharges qui attendent en paramètre la valeur booléenne sous la forme d'un boolean ou d'un BooleanSupplier et éventuellement un message affiché si le booléen vaut false sous la forme d'un String ou d'un Supplier<String>.
Exemple ( code Java 8 ) : |
package fr.jmdoudoux.dej.junit5;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assumptions.assumeTrue;
import org.junit.jupiter.api.Test;
public class MaClasseTest {
@Test
void testSousWindows() {
System.out.println(System.getenv("OS"));
assumeTrue(System.getenv("OS").startsWith("Windows"));
assertTrue(false);
}
}
La supposition assumeFalse permet d'exécuter la suite des traitements du test uniquement si la valeur booléenne fournie est false. C'est l'inverse de la supposition assumeTrue().
La méthode assumeFalse() possède plusieurs surcharges qui attendent en paramètre la valeur booléenne sous la forme d'un boolean ou d'un BooleanSupplier et éventuellement un message affiché si le booléen vaut false sous la forme d'un String ou d'un Supplier<String>.
La supposition assumingThat permet d'exécuter le traitement fourni uniquement si la valeur booléenne fournie est true.
La méthode assumingThat() possède deux surcharges qui attendent en paramètre la valeur booléenne sous la forme d'un boolean ou d'un BooleanSupplier et un objet de type Executable qui contient les traitements à exécuter.
Exemple ( code Java 8 ) : |
package fr.jmdoudoux.dej.junit5;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assumptions.assumingThat;
import java.io.File;
import org.junit.jupiter.api.Test;
public class MaClasseTest {
@Test
void testAvecSupposition() {
assumingThat(System.getenv("OS").startsWith("Windows"), () -> {
assertTrue(new File("C:/Windows").exists(), "Repertoire Windows inexistant");
});
assertTrue(true);
}
}
L'annotation @org.junit.jupiter.api.Disabled permet de désactiver un test.
Il est possible de fournir une description optionnelle de la raison de la désactivation
L'annotation @Disabled peut être utilisée sur une méthode ou sur une classe. L'utilisation sur une méthode désactive uniquement la méthode concernée.
Exemple ( code Java 8 ) : |
@Test
@Disabled("A écrire plus tard")
void monTest() {
fail("Non implémenté");
}
L'utilisation de l'annotation sur une classe désactive toutes les méthodes de tests de la classe.
Exemple ( code Java 8 ) : |
package fr.jmdoudoux.dej.junit5.testJUnit5;
import static org.junit.jupiter.api.Assertions.fail;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
@DisplayName("Ma classe de test JUnit5")
@Disabled
public class MonTest {
@Test
@DisplayName("Cas de test")
void monTest() {
fail("un test en echec");
}
}
L'annotation @Disabled de JUnit 5 est équivalente à l'annotation @Ignore de JUnit 4.
Les classes et les méthodes de tests peuvent être tagguées pour permettre d'utiliser ces tags ultérieurement pour déterminer les tests à exécuter. Ils peuvent par exemple être utilisés pour créer différents scénarios de tests ou pour exécuter les tests uniquement sur des environnements dédiés.
Le libellé d'un tag ne doit pas :
Les tags sont définis grâce à l'annotation @org.junit.jupiter.api.Tag. Elle peut être utilisée sur une classe de tests et/ou sur des méthodes d'une telle classe.
Exemple ( code Java 8 ) : |
package fr.jmdoudoux.dej.junit5;
import static org.junit.Assert.assertTrue;
import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.Test;
@Tag("principal")
public class MaClasseTest {
@Test
@Tag("general")
void testCas1() {
assertTrue(true);
}
@Test
@Tag("specifique")
void testCas2() {
assertTrue(true);
}
}
Il est possible d'utiliser plusieurs annotations @Tag sur un même élément pour définir plusieurs étiquettes.
Les Tags sont équivalents aux Categories de JUnit 4.
Dans le cycle de vie des instances de tests, les méthodes de tests sont des méthodes annotées avec @Test, @ParameterizedTest, @RepeatedTest, @TestFactory ou @TestTemplate.
Par défaut, JUnit créé une nouvelle instance pour chaque test avant d'exécuter la méthode concernée. Le but de ce comportement est d'exécuter le test de manière isolée et ainsi d'éviter les effets de bord liés à l'exécution des autres tests.
Il est possible de modifier ce comportement par défaut en utilisant l'annotation @org.junit.jupiter.api.TestInstance.
Elle ne possède qu'un seul attribut de type TestInstance.Lifecycle qui est une énumération possédant deux valeurs :
Valeur |
Rôle |
PER_CLASS |
Une seule instance est créée pour tous les tests d'une même classe |
PER_METHOD |
Une nouvelle instance est créée pour exécuter chaque méthode de test. C'est la valeur par défaut de l'attribut de l'annotation @TestInstance |
Donc pour modifier le comportement par défaut et demander à JUnit 5 d'exécuter toutes les méthodes de tests de la même instance, il faut annoter la classe avec @TestInstance(Lifecycle.PER_CLASS). Si les tests utilisent des variables d'instances, il est possible de réinitialiser leur état dans des méthodes annotées avec @BeforeEach et/ou @AfterEach.
Le mode PER_CLASS permet aussi :
Les tests imbriqués permettent de grouper des cas de test pour renforcer le lien qui existent entre-eux.
JUnit 5 permet de créer des tests imbriquées (nested tests) en utilisant une annotation @Nested sur une classe interne. Seules les classes internes non statiques peuvent être annotées avec @Nested.
Il permet d'utiliser des classes internes pour structurer le code de test de manière à conserver les tests en relation avec la classe de tests englobante. Il est par exemple possible de tester différents cas ou d'utiliser la même méthode de test dans la classe englobante et la classe interne.
Les tests imbriqués vont être exécutées en même temps que ceux de leur classe englobante.
Comme la classe de test imbriquée est une classe interne, elle a accès aux propriétés finales ou effectivement finales de la classe englobante.
Pour que la classe de tests imbriquées puisse avoir accès aux champs de la classe de tests englobante alors la classe imbriquée ne doit pas être statique. Si la classe imbriquée n'est pas static, alors il n'est pas possible d'utiliser de méthode statique dans la classe et donc d'avoir des méthodes annotées avec @BeforeAll et @AfterAll.
Exemple ( code Java 8 ) : |
package fr.jmdoudoux.dej.junit5;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
public class MonTest {
private int valeur = 0;
@BeforeAll
static void initAll() {
System.out.println("BeforeAll");
}
@BeforeEach
void init() {
System.out.println("BeforeEach");
valeur = 1;
}
@AfterEach
void tearDown() {
System.out.println("AfterEach");
valeur = 0;
}
@AfterAll
static void tearDownAll() {
System.out.println("AfterAll");
}
@Test
void simpleTest() {
System.out.println("SimpleTest valeur=" + valeur);
Assertions.assertEquals(1, valeur);
}
@Nested
class MonTestImbrique {
@BeforeEach
void init() {
System.out.println("BeforeEach imbrique");
valeur = 2;
}
@Test
void simpleTestImbrique() {
System.out.println("SimpleTest imbrique valeur=" + valeur);
Assertions.assertEquals(2, valeur);
}
}
}
Résultat : |
BeforeAll
BeforeEach
SimpleTest valeur=1
AfterEach
BeforeEach
BeforeEach imbrique
SimpleTest imbrique
valeur=2
AfterEach
AfterAll
Une classe de test imbriquée ne peut pas par défaut avoir de méthode static annotée : les annotations @BeforeAll et @AfterAll ne peuvent donc pas être utilisées. Pour pouvoir le faire, il faut annoter la classe imbriquée avec @TestInstance(Lifecycle.PER_CLASS) pour pouvoir utiliser les annotations @BeforeAll ou @AfterAll sur des méthodes qui ne sont pas static.
Exemple ( code Java 8 ) : |
package fr.jmdoudoux.dej.junit5;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestInstance;
import org.junit.jupiter.api.TestInstance.Lifecycle;
public class MonTest {
private int valeur = 0;
@BeforeAll
static void initAll() {
System.out.println("BeforeAll");
}
@BeforeEach
void init() {
System.out.println("BeforeEach");
valeur = 1;
}
@AfterEach
void tearDown() {
System.out.println("AfterEach");
valeur = 0;
}
@AfterAll
static void tearDownAll() {
System.out.println("AfterAll");
}
@Test
void simpleTest() {
System.out.println("SimpleTest valeur=" + valeur);
Assertions.assertEquals(1, valeur);
}
@Nested
@TestInstance(Lifecycle.PER_CLASS)
class MonTestImbrique {
@BeforeAll
void initAllImbrique() {
System.out.println("BeforAll imbrique");
}
@BeforeEach
void init() {
System.out.println("BeforeEach imbrique");
valeur = 2;
}
@Test
void simpleTestImbrique() {
System.out.println("SimpleTest imbrique valeur=" + valeur);
Assertions.assertEquals(2, valeur);
}
}
}
Résultat : |
BeforeAll
BeforeEach
SimpleTest valeur=1
AfterEach
BeforAll imbrique
BeforeEach
BeforeEach imbrique
SimpleTest imbrique valeur=2
AfterEach
AfterAll
Il est possible d'inclure un test imbriqué dans un autre test imbriqué.
Dans les versions antérieures à la version 5 de JUnit, les constructeurs et les méthodes de test des classes de tests ne pouvaient pas avoir de paramètres pour être exécutées par le Runner standard.
JUnit 5 permet de mettre en oeuvre l'injection de dépendances via des paramètres pour les constructeurs et les méthodes des classes de tests. L'interface org.junit.jupiter.api.extension.ParameterResolver permet de définir des fonctionnalités pour résoudre dynamiquement des paramètres au runtime.
Les constructeurs et les méthodes annotées @Test, @TestFactory, @BeforeEach, @AfterEach, @BeforeAll ou @AfterAll d'une classe de tests peuvent avoir un ou plusieurs paramètres. Ces paramètres doivent être résolus à l'exécution par une instance de type ParameterResolver préalablement enregistrée.
Par défaut, JUnit 5 enregistre automatiquement 3 ParameterResolver :
Un TestInfoParameterResolver permet de résoudre et d'injecter une instance de type TestInfo. Une instance de type TestInfo permet d'obtenir des informations sur le test en cours d'exécution.
L'interface TestInfo définit plusieurs méthodes :
Méthode |
Rôle |
String getDisplayName() |
Obtenir le nom du test courant |
Set<String> getTags() |
Obtenir une collection des tags associés au test courant |
Optional<Class<?>> getTestClass() |
Obtenir la classe utilisée pour le test courant si disponible |
Optional<Method> getTestMethod() |
Obtenir la méthode utilisée pour le test courant si disponible |
Exemple ( code Java 8 ) : |
package fr.jmdoudoux.dej.junit5;
import static org.junit.Assert.assertTrue;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestInfo;
@DisplayName("Ma classe de test JUnit5")
class MonTestSimple {
@Test
@Tag("monTag")
@DisplayName("mon test")
void monTest(TestInfo testInfo) {
System.out.println("Display name : " + testInfo.getDisplayName());
System.out.println("Classe de test : " + testInfo.getTestClass().get().getName());
System.out.println("Methode de test : " + testInfo.getTestMethod().get().getName());
System.out.println("Tag : " + testInfo.getTags().toArray()[0]);
System.out.println();
assertTrue(true);
}
}
Résultat : |
Display name : mon test
Classe de test : fr.jmdoudoux.dej.junit5.MonTestSimple
Methode de test :
monTest
Tag : monTag
Un RepetitionInfoParameterResolver permet de résoudre et d'injecter une instance de type RepetitionInfo dans une méthode annotée avec @RepeatedTest, @BeforeEach ou @AfterEach. Une instance de type RepetitionInfo permet d'obtenir des informations sur l'itération du test répété en cours d'exécution.
L'interface RepetitionInfo définit deux méthodes :
Méthode |
Rôle |
int getCurrentRepetition() |
Obtenir l'itération courante d'un test répété défini par une méthode annotée avec @RepeatedTest |
int getTotalRepetitions() |
Obtenir le nombre d'itération d'un test répété défini par une méthode annotée avec @RepeatedTest |
Un TestReporterParameterResolver permet de résoudre et d'injecter une instance de type TestReporter dans une méthode annotée avec @Test, @BeforeEach ou @AfterEach. Une instance de type TestReporter permet d'ajouter des informations sur le test courant qui pourront être exploitées par la méthode reportingEntryPublished() de la classe TestExecutionListener pour générer le rapport d'exécution des tests.
L'interface TestReport est une interface fonctionnelle qui définit deux méthodes :
Méthode |
Rôle |
void publishEntry(Map<String, String> values) |
Publier les clés/valeurs fournies dans la Map |
default void publishEntry(String key, String value) |
Publier la paire clé/valeur fournie |
Exemple ( code Java 8 ) : |
package fr.jmdoudoux.dej.junit5;
import static org.junit.Assert.assertTrue;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestReporter;
class MonTestSimple {
@Test
void monTest(TestReporter testReporter) {
testReporter.publishEntry("maCle", "maValeur");
assertTrue(true);
}
}
D'autres ParameterResolver peuvent être enregistrés avec l'annotation @ExtendWith pour pouvoir être utilisés.
JUnit Jupiter permet une exécution répétée un certain nombre de fois d'une méthode de test en l'annotant avec @RepeatedTest.
L'annotation @RepeatedTest possède deux attributs :
Attribut |
Rôle |
int value |
Le nombre de répétitions à opérer. Obligatoire |
String name |
Le libellé de chaque test exécuté |
Une méthode annotée avec @RepeatedTest doit respecter plusieurs contraintes :
Si tel n'est pas le cas, la méthode ne sera tout simplement pas exécutée par le moteur d'exécution des tests.
Chaque exécution du test se comporte comme celle d'un test standard : ainsi les méthodes annotées avec @BeforeEach et @AfterEach seront invoquée avant chaque exécution répétée.
Exemple ( code Java 8 ) : |
@DisplayName("test addition repété")
@RepeatedTest(3)
void testRepete() {
Assertions.assertEquals(2, 1 + 1, "Valeur obtenue erronée");
}
Il est possible de personnaliser le libellé de chaque exécution du test répété en utilisant l'attribut name de l'annotation @RepeatedTest. Le libellé fourni comme valeur peut contenir trois placeholders qui seront remplacés par leurs valeurs courantes au moment de l'exécution
Placeholder |
Rôle |
{displayName} |
Le libellé du test défini avec @DisplayName |
{currentRepetition} |
L'itération courante |
{totalRepetitions} |
Le nombre total de répétitions |
Il existe aussi deux libellés prédéfinis :
Exemple ( code Java 8 ) : |
@DisplayName("test addition repété")
@RepeatedTest(value = 3, name = RepeatedTest.LONG_DISPLAY_NAME)
void testRepete() {
Assertions.assertEquals(2, 1 + 1, "Valeur obtenue erronée");
}
Pour obtenir des informations sur l'itération courante, il est possible d'ajouter un paramètre de type RepetitionInfo à la méthode annotée avec @RepeatedTest. Une instance de ce type sera alors injectée par le moteur d'exécution au moment de l'invocation de la méthode.
Un objet de type RepeatedInfo peut être utilisé comme paramètre dans une méthode annotée avec @RepeatedTest, @BeforeEach et @AfterEach.
L'interface RepetitionInfo définit deux méthodes :
Méthode |
Rôle |
int getCurrentRepetition() |
Obtenir l'itération courante du test répété |
int getTotalRepetitions() |
Obtenir le nombre total de répétitions du test répété |
Important : l'invocation de méthode annotée avec @BeforeEach et @AfterEach ayant un paramètre de type RepetitionInfo relative à des tests non répétés lèvent une exception de type ParameterResolutionException.
Exemple ( code Java 8 ) : |
@DisplayName("test addition repété")
@RepeatedTest(value = 3)
void testRepete(RepetitionInfo repInfo) {
System.out.println("iteration courante : " + repInfo.getCurrentRepetition());
System.out.println("nombre de repetition :" + repInfo.getTotalRepetitions());
Assertions.assertEquals(2, 1 + 1, "Valeur obtenue erronée");
}
Les tests paramétrés permettent d'exécuter un même test plusieurs fois avec différentes valeurs qui lui sont passés en paramètres.
La méthode de test doit être annotée avec @org.junit.jupiter.params.ParameterizedTest. Il est aussi nécessaire de déclarer une source de données permettant d'obtenir les différentes valeurs à l'exécution des tests.
Chaque exécution du test avec les différentes valeurs est signalée de manière séparée.
Pour utiliser les tests paramétrés, il faut ajouter la dépendance junit-jupiter-params.
Les paramètres des tests sont fournis grâce à une source. JUnit Jupiter propose en standard plusieurs annotations pour différents types de source dans la package org.junit.jupiter.params.provider.
Annotation |
Rôle |
@ValueSource |
Une source de données simple sous la forme d'un tableau de chaînes de caractères ou de primitifs (int, long ou double) |
@EnumSource |
Une source de données simple sous la forme d'une énumération |
@MethodSource |
Une source de données dont les valeurs sont fournies par une méthode |
@CsvSource |
Une source de données dont les valeurs sont fournies sous la forme de chaînes de caractères dans laquelle chaque argument est séparé par une virgule |
@CsvSourceFile |
Une source de données dont les valeurs sont fournies sous la forme d'un ou plusieurs fichiers CSV |
@ArgumentsSource |
Une source de données qui est une méthode d'une instance de type ArgumentProvider |
L'annotation @ValueSource est une source de données simple sous la forme d'un tableau de chaînes de caractères ou de primitifs (int, long ou double). Une seule valeur ne pourra être fournie pour chaque test.
L'annotation @ValueSource possède plusieurs attributs pour permettre de fournir le tableau de valeurs :
Attributs |
Rôle |
doubles |
Fournir un tableau de valeurs de type double |
longs |
Fournir un tableau de valeurs de type long |
ints |
Fournir un tableau de valeurs de type int |
strings |
Fournir un tableau de valeurs de chaînes de caractères |
Attention : il ne faut utiliser qu'un seul de ses attributs à la fois. Dans le cas contraire, le test échoue en levant une exception de type JUnitException.
Exemple ( code Java 8 ) : |
@ParameterizedTest
@ValueSource(ints = { 1, 2, 3 })
void testParametreAvecValueSource(int valeur) {
assertEquals(valeur + valeur, valeur * 2);
}
L'annotation @EnumSource est une source de données simple sous la forme d'une énumération. Les valeurs définies dans l'énumération seront fournies à la méthode, une pour chaque exécution.
L'annotation @EnumSource possède plusieurs attributs :
Attributs |
Rôle |
Class<? extends Enum<?>> value |
Préciser le type de l'énumération dont les valeurs seront utilisées pour les tests. Obligatoire |
EnumSource.Mode mode |
Préciser le mode de sélection des valeurs de l'énumération. EnumSource.Mode.INCLUDE par défaut |
String[] names |
Le nom des valeurs à utiliser ou des expressions régulières selon le mode utilisé. Un tableau vide par défaut. Si le tableau est vide, alors toutes les valeurs de l'énumération sont utilisées. |
Exemple ( code Java 8 ) : |
@ParameterizedTest
@EnumSource(Month.class)
void testParametreAvecEnumSource(Month mois) {
System.out.println(mois);
Assertions.assertNotNull(mois);
}
Résultat : |
JANUARY
FEBRUARY
MARCH
APRIL
MAY
JUNE
JULY
AUGUST
SEPTEMBER
OCTOBER
NOVEMBER
DECEMBER
L'attribut names permet de préciser les différents éléments de l'énumération qui devront être fournis en paramètre.
Exemple ( code Java 8 ) : |
@ParameterizedTest
@EnumSource(value = Month.class, names = { "JANUARY", "FEBRUARY", "MARCH" })
void testParametreAvecEnumSource(Month mois) {
System.out.println(mois);
Assertions.assertNotNull(mois);
}
Résultat : |
JANUARY
FEBRUARY
MARCH
L'attribut mode permet d'avoir un contrôle sur les valeurs de l'énumération fournies en paramètre.
L'énumération EnumSource.Mode possède plusieurs valeurs :
Valeur |
Rôle |
EXCLUDE |
Fournir tous les éléments de l'énumération sauf ceux dont le nom est précisé dans l'attribut names |
INCLUDE |
Ne fournir que les éléments de l'énumération dont le nom est précisé dans l'attribut names |
MATCH_ALL |
Ne fournir que les éléments de l'énumération dont le nom correspond aux motifs fournis dans l'attribut names |
MATCH_ANY |
Ne fournir que les éléments de l'énumération dont le nom correspond à un des motifs fournis dans l'attribut names |
Exemple ( code Java 8 ) : |
@ParameterizedTest
@EnumSource(value = Month.class, mode = Mode.MATCH_ALL, names = { "^J.+$" })
void testParametreAvecEnumSource(Month mois) {
System.out.println(mois);
Assertions.assertNotNull(mois);
}
Résultat : |
JANUARY
JUNE
JULY
L'annotation @MethodSource est une source de données dont les valeurs sont fournies par une méthode sous la forme d'un Stream, d'un Stream pour type primitif, d'un Iterable, d'un Iterator ou d'un tableau.
La méthode utilisée ne doit pas avoir de paramètre et doit être static sauf si la classe est annotée avec @TestInstance(Lifecycle.PER_CLASS).
Si le cas de tests n'a besoin que d'un seul paramètre, il suffit de retourner un ensemble de données de ce type.
Exemple ( code Java 8 ) : |
package fr.jmdoudoux.dej.junit5;
import static org.junit.jupiter.api.Assertions.assertTrue;
import java.util.stream.Stream;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.MethodSource;
public class MaClasseTest {
@ParameterizedTest
@MethodSource("fournirDonnees")
void testExecuter(String element) {
assertTrue(element.startsWith("elem"));
}
static Stream<String> fournirDonnees() {
return Stream.of("elem1", "elem2");
}
}
Si la méthode de test requière plusieurs arguments, la source de données retourne des instances qui sont un tableau avec les différentes valeurs.
Exemple ( code Java 8 ) : |
package fr.jmdoudoux.dej.junit5;
import static org.junit.jupiter.api.Assertions.assertTrue;
import java.util.Arrays;
import java.util.List;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.MethodSource;
public class MaClasseTest {
@ParameterizedTest
@MethodSource("fournirDonnees")
void testTraiter(int index, String element) {
assertTrue(index > 0);
assertTrue(element.startsWith("elem"));
}
static List<Object[]> fournirDonnees() {
return Arrays.asList(new Object[][] { { 1, "elem1" }, { 2, "elem2" } });
}
}
Il est aussi possible que la source de données retourne des instances de type org.junit.jupiter.params.provider.Arguments
L'interface Arguments propose une fabrique statique of() qui attend en paramètre un varargs d'Object pour créer des instances.
Exemple ( code Java 8 ) : |
package fr.jmdoudoux.dej.junit5;
import static org.junit.jupiter.api.Assertions.assertTrue;
import java.util.stream.Stream;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
public class MaClasseTest {
@ParameterizedTest
@MethodSource("fournirDonnees")
void testTraiter(int index, String element) {
assertTrue(index > 0);
assertTrue(element.startsWith("elem"));
}
static Stream<Arguments> fournirDonnees() {
return Stream.of(Arguments.of(1, "elem1"), Arguments.of(2, "elem2"));
}
}
L'annotation @CsvSource est une source de données dont les valeurs sont fournies sous la forme de chaînes de caractères dans laquelle chaque arguments est séparés par une virgule.
La valeur par défaut de l'annotation @CsvSource est un tableau de chaînes de caractères. Chaque chaîne de caractères sera utilisée comme source de données pour une exécution du cas de test : chacune des valeurs requises doit être séparées par une virgule.
Exemple ( code Java 8 ) : |
package fr.jmdoudoux.dej.junit5;
import static org.junit.jupiter.api.Assertions.assertEquals;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvSource;
public class MaClasseTest {
@DisplayName("Addition")
@ParameterizedTest()
@CsvSource({ "1, 1", "1, 2", "2, 3" })
void testAdditioner(int a, int b) {
int attendu = a + b;
assertEquals(attendu, a + b);
}
}
L'annotation @CsvSourceFile est une source de données dont les valeurs sont fournies sous la forme d'un ou plusieurs fichiers CSV. Chaque ligne du fichier CSV sera utilisé comme source de données pour une exécution du cas de test : chacune des valeurs requises doit être séparée par une virgule.
Exemple : le fichier additionner_source.csv
Résultat : |
1,1
1,2
2,3
Le fichier CSV doit être accessible dans le classpath pour permettre son chargement.
L'attribut resources de l'annotation @ParameterizedTest permet de préciser un ou plusieurs fichiers CSV sous la forme d'un tableau de chaînes de caractères.
Exemple ( code Java 8 ) : |
package fr.jmdoudoux.dej.junit5;
import static org.junit.jupiter.api.Assertions.assertEquals;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvFileSource;
public class MaClasseTest {
@DisplayName("Addition")
@ParameterizedTest()
@CsvFileSource(resources = "additionner_source.csv")
void testAdditionner(int a, int b) {
int attendu = a + b;
assertEquals(attendu, a + b);
}
}
L'annotation @org.junit.jupiter.params.provider.ArgumentsSource permet de préciser la classe de type org.junit.jupiter.params.provider.ArgumentProvider dont une instance sera utilisée comme source de données.
L'interface ArgumentProvider ne définit qu'une seule méthode
Méthode |
Rôle |
Stream<? extends Arguments> provideArguments(ExtensionContext context) |
Renvoyer un Stream qui fournit les arguments passés à une méthode de test annotée avec @ParameterizedTest |
Une implémentation de l'interface ArgumentsProvider doit proposer un constructeur par défaut.
Exemple ( code Java 8 ) : |
package fr.jmdoudoux.dej.junit5;
import static org.junit.jupiter.api.Assertions.assertTrue;
import java.util.stream.Stream;
import org.junit.jupiter.api.extension.ExtensionContext;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.ArgumentsProvider;
import org.junit.jupiter.params.provider.ArgumentsSource;
public class MaClasseTest {
@ParameterizedTest
@ArgumentsSource(MonArgumentsProvider.class)
void testAvecArgumentsSource(String valeur) {
assertTrue(valeur.startsWith("elem"));
}
static class MonArgumentsProvider implements ArgumentsProvider {
@Override
public Stream<? extends Arguments> provideArguments(ExtensionContext context) {
return Stream.of("elem1", "elem2", "elem3").map(Arguments::of);
}
}
}
Pour permettre le support des valeurs de certaines sources de données fournies sous la forme de chaînes de caractères, JUnit Jupiter effectue des conversions au besoin vers des types selon ceux utilisés dans les paramètres de la méthode de test.
JUnit Jupiter supporte la conversion implicite pour plusieurs types :
Type cible |
Exemple |
|
Valeur fournie |
Valeur convertie |
|
Boolean, boolean |
"true" |
true |
Byte, byte |
"1" |
1 (byte) |
Character, char |
"a" |
'a' |
Short, short |
"1" |
1 (short) |
Integer, int |
"1" |
1 |
Long, long |
"1" |
1L |
Float, float |
"1.0" |
1.0f |
Double, double |
"1.0" |
1.0d |
Enum |
"APRIL" |
Month.APRIL |
java.time.Instant |
"1970-01-01T00:00:00Z" |
Instant.ofEpochMilli(0) |
java.time.LocalDate |
"2017-12-25" |
LocalDate.of(2017, 12, 25) |
java.time.LocalDateTime |
"2017-12-25T13:30:59.524" |
LocalDateTime.of(2017, 12, 25, 13, 30, 59, 524_000_000) |
java.time.LocalTime |
"13:30:59.524" |
LocalTime.of(13, 30, 59, 524_000_000) |
java.time.OffsetDateTime |
"2017-12-25T13:30:59.524Z" |
OffsetDateTime.of(2017, 12, 25, 13, 30, 59, 524_000_000, ZoneOffset.UTC) |
java.time.OffsetTime |
"13:30:59.524Z" |
OffsetTime.of(13, 30, 59, 524_000_000, ZoneOffset.UTC) |
java.time.Year |
"2017" |
Year.of(2017) |
java.time.YearMonth |
"2017-12" |
YearMonth.of(2017, 12) |
java.time.ZonedDateTime |
"2017-12-25T13:30:59.524Z" |
ZonedDateTime.of(2017, 12, 25, 13, 30, 59, 524_000_000, ZoneOffset.UTC) |
Exemple ( code Java 8 ) : |
package fr.jmdoudoux.dej.junit5;
import static org.junit.Assert.assertNotNull;
import java.time.Month;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;
class MonTestSimple {
@ParameterizedTest
@ValueSource(strings = { "JANUARY", "FEBRUARY", "MARCH" })
void testWithImplicitArgumentConversion(Month mois) {
assertNotNull(mois.name());
}
}
La conversion implicite des arguments est réalisée par une classe qui implémente l'interface ArgumentConverter.
L'interface ArgumentConverter ne définit qu'une seule méthode :
Méthode |
Rôle |
Object convert(Object source, ParameterContext context) |
Convertir l'objet source selon le contexte fourni |
La classe abstraite SimpleArgumentConverter qui implémente l'interface ArgumentConverter peut être utilisée comme classe de base pour une implémentation d'un Converter.
La classe DefaultArgumentConverter hérite de la classe SimpleArgumentConverter et est le Converter par défaut permettant de convertir une chaîne de caractères vers les wrappers des principaux types primitifs.
Pour demander l'application de la conversion sur un paramètre, il faut utiliser l'annotation @ConvertWith en lui passant en paramètre la classe de l'implémentation de type ArgumentConverter.
Le module junit-jupiter-params ne propose qu'une seule implémentation de type ArgumentConverter : JavaTimeArgumentConverter. L'utilisation de cet ArgumentConverter se fait en grâce à l'annotation @JavaTimeConversionPattern qui attend comme valeur le format de la donnée temporelle.
Exemple ( code Java 8 ) : |
package fr.jmdoudoux.dej.junit5;
import static org.junit.jupiter.api.Assertions.assertEquals;
import java.time.LocalDate;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.converter.JavaTimeConversionPattern;
import org.junit.jupiter.params.provider.ValueSource;
class MonTestSimple {
@ParameterizedTest
@ValueSource(strings = "25/12/2017")
void testWithExplicitJavaTimeConverter(@JavaTimeConversionPattern("dd/MM/yyyy")
LocalDate date) {
assertEquals(2017, date.getYear());
}
}
Par défaut, le nom de chaque cas de test est composé de l'index du cas suivi d'une représentation des arguments utilisés lors de son exécution.
Il est possible de personnaliser le libellé du test en utilisant l'attribut name de l'annotation @ParameterizedTest.
Plusieurs placeholders peuvent être utilisés dans la chaîne définissant l'attribut name : leur valeur correspondante sera utilisée dans le libellé affiché :
Placeholder |
Rôle |
{index} |
L'indice du test courant, le premier ayant l'indice 1 |
{arguments} |
La liste des valeurs de tous les arguments séparées par une virgule |
{0}, {1}, ... |
La valeur de l'argument dont l'index est fourni entre les accolades |
Exemple ( code Java 8 ) : |
package fr.jmdoudoux.dej.junit5;
import static org.junit.jupiter.api.Assertions.assertEquals;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvSource;
public class MaClasseTest {
@DisplayName("Addition")
@ParameterizedTest(name = "{index} : l''addition de {0} et {1}")
@CsvSource({ "1, 1", "1, 2", "2, 3" })
void testAdditioner(int a, int b) {
int attendu = a + b;
assertEquals(attendu, a + b);
}
}
Les tests dynamiques sont une nouvelle fonctionnalité de JUnit 5 qui permet la création dynamique de tests à l'exécution. Les tests standard définis avec l'annotation @Test sont statiques : l'intégralité de leur définition doit être fournie à la compilation.
Les tests dynamiques sont des tests qui sont générés à l'exécution par une méthode de type fabrique qui est annotée avec @TestFactory. Les méthodes annotées avec @TestFactory ne sont donc pas des cas de tests mais des fabriques pour fournir un ensemble de cas de tests. Les tests dynamiques permettent par exemple d'obtenir les données requises par les cas de tests d'une source externe.
Le code du test doit être encapsulé dans une instance de type DynamicTest qui est créé dynamiquement à l'exécution.
Une méthode annotée avec @TestFactory ne peut pas être static ou private et peut retourner un objet de type :
Si la méthode renvoie un objet d'un autre type alors une exception de type JUnitException est levée.
Une instance de DynamicTest peut être créée en utilisant la méthode statique dynamicTest(String, Executable) de la classe DynamicTest. Cette méthode est une fabrique qui attend en paramètre le nom du test et le code à exécuter. Le test est fourni sous la forme d'une interface fonctionnelle Executable ce qui permet de fournir l'implémentation sous la forme d'une expression Lambda.
La fabrique peut renvoyer une Collection de DynamicTest
Exemple ( code Java 8 ) : |
package fr.jmdoudoux.dej.junit5;
import static org.junit.jupiter.api.Assertions.assertEquals;
import java.util.ArrayList;
import java.util.Collection;
import org.junit.jupiter.api.DynamicTest;
import org.junit.jupiter.api.TestFactory;
class MonTestSimple {
@TestFactory
Collection<DynamicTest> dynamicTestsAvecCollection() {
Collection<DynamicTest> resultat = new ArrayList<>();
for (int i = 1; i <= 5; i++) {
int val = i;
resultat.add(DynamicTest.dynamicTest("Ajout " + val + "+" + val,
() -> assertEquals(val * 2, val + val)));
}
return resultat;
}
}
La fabrique peut aussi renvoyer un Iterable de DynamicTest
Exemple ( code Java 8 ) : |
package fr.jmdoudoux.dej.junit5;
import static org.junit.jupiter.api.Assertions.assertEquals;
import java.util.ArrayList;
import java.util.List;
import org.junit.jupiter.api.DynamicTest;
import org.junit.jupiter.api.TestFactory;
class MonTestSimple {
@TestFactory
Iterable<DynamicTest> dynamicTestAvecIterable() {
List<DynamicTest> resultat = new ArrayList<>();
for (int i = 1; i <= 5; i++) {
int val = i;
resultat.add(DynamicTest.dynamicTest("Ajout " + val + "+" + val,
() -> assertEquals(val * 2, val + val)));
}
return resultat;
}
}
La fabrique peut aussi renvoyer un Iterator de DynamicTest
Exemple ( code Java 8 ) : |
package fr.jmdoudoux.dej.junit5;
import static org.junit.jupiter.api.Assertions.assertEquals;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import org.junit.jupiter.api.DynamicTest;
import org.junit.jupiter.api.TestFactory;
class MonTestSimple {
@TestFactory
Iterator<DynamicTest> dynamicTestsAvecIterator() {
List<DynamicTest> resultat = new ArrayList<>();
for (int i = 1; i <= 5; i++) {
int val = i;
resultat.add(DynamicTest.dynamicTest("Ajout " + val + "+" + val,
() -> assertEquals(val * 2, val + val)));
}
return resultat.iterator();
}
}
La fabrique peut enfin renvoyer un Stream de DynamicTest
Exemple ( code Java 8 ) : |
package fr.jmdoudoux.dej.junit5;
import static org.junit.jupiter.api.Assertions.assertEquals;
import java.util.stream.IntStream;
import java.util.stream.Stream;
import org.junit.jupiter.api.DynamicTest;
import org.junit.jupiter.api.TestFactory;
class MonTestSimple {
@TestFactory
Stream<DynamicTest> dynamicTestsAvecStream() {
return IntStream.rangeClosed(1,5)
.mapToObj(val -> DynamicTest.dynamicTest("Ajout " + val + "+" + val,
() -> assertEquals(val * 2, val + val)));
}
}
Si la fabrique renvoie un Stream, sa méthode close() sera invoquée à la fin de l'exécution des tests dynamiques qu'il contient, ce qui est important si la source du Stream requiert sa fermeture.
Le moteur d'exécution de JUnit va invoquer les méthodes annotées avec @TestFactory, ajouter le DynamicTest obtenu dans les tests et l'exécuter.
Les méthodes annotées avec @TestFactory peuvent avoir si besoin des paramètres dont les valeurs devront être injectées grâce à des ParameterResolvers.
L'exécution des DynamicTest est différente de celle des tests standards : les cas de tests dynamiques ne peuvent pas avoir de méthodes du cycle de vie invoquées. Les méthodes annotées avec @BeforeEach et @AfterEach sont invoquées pour la méthode annotée avec @TestFactory mais elles ne sont pas invoquées lors de l'exécution des cas de tests créés dynamiquement.
JUnit Jupiter permet d'utiliser les annotations @Test, @RepeatedTest, @ParameterizedTest, @TestFactory, @TestTemplate, @BeforeEach et @AfterEach sur des méthodes par défaut d'une interface.
Il est aussi possible d'utiliser les annotations @BeforeAll et @AfterAll sur des méthodes static ou default d'une interface qui doit être annotée dans ce cas avec @TestInstance(Lifecycle.PER_CLASS).
Exemple ( code Java 8 ) : |
package fr.jmdoudoux.dej.junit5;
import static org.junit.Assert.assertTrue;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.DynamicTest.dynamicTest;
import java.util.Arrays;
import java.util.Collection;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DynamicTest;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestFactory;
import org.junit.jupiter.api.TestInfo;
import org.junit.jupiter.api.TestInstance;
import org.junit.jupiter.api.TestInstance.Lifecycle;
@TestInstance(Lifecycle.PER_CLASS)
interface MonInterface {
@BeforeAll
default void beforeAll() {
System.out.println("Before all");
}
@AfterAll
default void afterAll() {
System.out.println("After all");
}
@BeforeEach
default void beforeEach(TestInfo testInfo) {
System.out.println("Before test " + testInfo.getDisplayName());
}
@AfterEach
default void afterEach(TestInfo testInfo) {
System.out.println("After test " + testInfo.getDisplayName());
}
@TestFactory
default Collection<DynamicTest> dynamicTests() {
return Arrays.asList(dynamicTest("true", () ->assertTrue(true)),
dynamicTest("false", () -> assertFalse(false)));
}
@Test
default void monPremierTest() {
assertTrue(true);
}
}
Il est aussi possible d'utiliser les annotations @ExtendWith et @Tag sur une interface, ce qui permettra aux classes qui l'implémente d'hériter de ces annotations.
Pour exécuter les tests, il est nécessaire d'écrire une classe qui implémente l'interface pour permettre au moteur d'exécution la création d'une instance.
Exemple ( code Java 8 ) : |
package fr.jmdoudoux.dej.junit5;
import static org.junit.jupiter.api.Assertions.assertTrue;
import org.junit.jupiter.api.Test;
class TestAvecInterface implements MonInterface {
@Test
void monSecondTest() {
assertTrue(true);
}
}
Résultat : |
Before all
Before test dynamicTests()
After test dynamicTests()
Before test monPremierTest()
After test monPremierTest()
Before test monSecondTest()
After test monSecondTest()
After all
JUnit 5 permet la création de suites de tests (tests suite) qui sont une agrégation de multiples classes de tests qui pourront être exécutées ensembles.
Une suite de tests peut être composée de classes provenant de différents packages. Attention dans ce cas, les règles de visibilité doivent être respectées. Dans d'autres packages, seuls les tests de classes et méthodes de test publiques seront exécutés.
JUnit 5 propose plusieurs annotations pour définir et configurer une suite de tests :
Annotation Type |
Description |
@ExcludeClassNamePatterns |
Préciser une ou plusieurs expressions régulières que le nom pleinement qualifié des classes à exclure dans la suite doivent respecter |
@ExcludePackages |
Préciser des packages dont les tests doivent être ignorés lors de l'exécution de la suite |
@ExcludeTags |
Préciser des tags dont les tests doivent être ignorés lors de l'exécution de la suite |
@IncludeClassNamePatterns |
Préciser une ou plusieurs expressions régulières que le nom pleinement qualifié des classes à inclure dans la suite doivent respecter |
@IncludePackages |
Préciser des packages et leur sous-packages dont les tests doivent être utilisés lors de l'exécution de la suite |
@IncludeTags |
Préciser des tags dont les tests doivent être utilisés lors de l'exécution de la suite |
@SelectClasses |
Préciser un ensemble de classes à sélectionner lors de l'exécution de la suite de tests |
@SelectPackages |
Préciser des packages dont les tests doivent être utilisés lors de l'exécution de la suite |
@UseTechnicalNames |
Demander d'utiliser le nom technique (nom pleinement qualifié de la classe) plutôt que le nom par défaut |
Ces annotations peuvent être utilisées pour sélectionner les packages, les classes et les méthodes à inclure dans la suite de tests en utilisant des filtres pour inclure ou exclure ces éléments.
Il est possible d'utiliser l'annotation @SelectPackages sur une classe annotée avec @RunWith(JUnitPlatform.class) pour sélectionner les classes à inclure dans la suite.
Il est possible de ne préciser qu'un seul package comme valeur de l'attribut de l'annotation @SelectPackages : la suite sera alors composée des classes du package et de ses sous-packages. Par défaut, les classes de tests exécutées doivent avoir un nom qui se terminent par Test ou Tests.
Exemple ( code Java 8 ) : |
package fr.jmdoudoux.dej.junit5suite;
import org.junit.platform.runner.JUnitPlatform;
import org.junit.platform.suite.api.SelectPackages;
import org.junit.runner.RunWith;
@RunWith(JUnitPlatform.class)
@SelectPackages("fr.jmdoudoux.dej.junit5")
public class MaSuiteDeTests {
}
Il est aussi possible de passer plusieurs classes comme attribut de l'annotation @SelectPackages.
Exemple ( code Java 8 ) : |
package fr.jmdoudoux.dej.junit5suite;
import org.junit.platform.runner.JUnitPlatform;
import org.junit.platform.suite.api.SelectPackages;
import org.junit.runner.RunWith;
@RunWith(JUnitPlatform.class)
@SelectPackages({"fr.jmdoudoux.dej.junit5.modele","fr.jmdoudoux.dej.junit5.service"})
public class MaSuiteDeTests {
}
Les classes de tests exécutées sont celles contenues dans les packages précisés et leurs sous-packages.
Il est possible d'utiliser l'annotation @SelectClasses sur une classe annotée avec @RunWith(JUnitPlatform.class) pour sélectionner les classes à inclure dans la suite.
Il est possible de ne préciser qu'une seule classe comme valeur de l'attribut de l'annotation @SelectClasses : la suite sera alors uniquement composée de la classe précisée.
Exemple ( code Java 8 ) : |
package fr.jmdoudoux.dej.junit5;
import org.junit.platform.runner.JUnitPlatform;
import org.junit.platform.suite.api.SelectClasses;
import org.junit.runner.RunWith;
@RunWith(JUnitPlatform.class)
@SelectClasses(MonTest.class)
public class MaSuiteDeTests {
}
Il est aussi possible de passer plusieurs classes comme attribut de l'annotation @SelectClasses
Exemple ( code Java 8 ) : |
package fr.jmdoudoux.dej.junit5;
import org.junit.platform.runner.JUnitPlatform;
import org.junit.platform.suite.api.SelectClasses;
import org.junit.runner.RunWith;
@RunWith(JUnitPlatform.class)
@SelectClasses({ MonTest.class, MonTestSimple.class, MaClasseTest.class })
public class MaSuiteDeTests {
}
Par défaut, l'annotation @SelectPackages recherche les classes de tests à inclure dans les packages précisés et leurs sous-packages. Les annotations @IncludePackages et @ExcludePackages permettent respectivement d'inclure uniquement ou d'exclure un ou plusieurs sous-packages particuliers.
Exemple ( code Java 8 ) : |
package fr.jmdoudoux.dej.junit5suite;
import org.junit.platform.runner.JUnitPlatform;
import org.junit.platform.suite.api.SelectPackages;
import org.junit.platform.suite.api.IncludePackages;
import org.junit.runner.RunWith;
@RunWith(JUnitPlatform.class)
@SelectPackages("fr.jmdoudoux.dej.junit5")
@IncludePackages("fr.jmdoudoux.dej.junit5.service")
public class MaSuiteDeTests {
}
Dans l'exemple ci-dessus, seules les classes du sous-packages service seront incluses dans la suite.
L'annotation @ExcludePackages s'utilise de manière similaire pour exclure des classes de tests d'un ou plusieurs packages de la suite.
Il n'est pas toujours possible de fournir explicitement le nom de toutes les classes à inclure/exclure dans la suite notamment si ce nombre est important.
Les annotations @IncludeClassNamePatterns et @ExcludeClassNamePatterns permettent respectivement d'inclure ou d'exclure des classes selon que leur nom respecte un ou plusieurs motifs.
Exemple ( code Java 8 ) : |
package fr.jmdoudoux.dej.junit5suite;
import org.junit.platform.runner.JUnitPlatform;
import org.junit.platform.suite.api.IncludeClassNamePatterns;
import org.junit.platform.suite.api.SelectPackages;
import org.junit.runner.RunWith;
@RunWith(JUnitPlatform.class)
@SelectPackages("fr.jmdoudoux.dej.junit5")
@IncludeClassNamePatterns({ "^.*Simple$" })
public class MaSuiteDeTests {
}
L'exemple ci-dessus n'inclut que les classes de tests dont le nom se termine par Simple.
L'annotation @ExcludeClassNamePattern s'utilise de manière similaire pour exclure des classes de tests de la suite si leur nom respecte le ou les motifs.
Plusieurs motifs peuvent être fournis : dans ce cas, les différents motifs sont combinés avec un opérateur OR. Si le nom pleinement qualifié de la classe respecte au moins un motif, la classe est incluse/exclue de la suite de test.
Il est possible de n'exécuter que les tests qui sont taggués ou au contraire d'exclure des tests taggués.
Cela peut par exemple permettre de définir des suites de tests qui ne seront exécutées que dans certaines circonstances ou dans un environnement d'exécution particulier.
Les annotations @IncludeTags et @ExcludeTags permettent de créer des plans de tests dans lesquels seront inclus ou exclus les tests selon leurs tags.
Exemple ( code Java 8 ) : |
package fr.jmdoudoux.dej.junit5suite;
import org.junit.platform.runner.JUnitPlatform;
import org.junit.platform.suite.api.IncludeTags;
import org.junit.platform.suite.api.SelectPackages;
import org.junit.runner.RunWith;
@RunWith(JUnitPlatform.class)
@SelectPackages("fr.jmdoudoux.dej.junit5")
@IncludeTags("MonTag")
public class MaSuiteDeTests {
}
Pour préciser plusieurs tags, il suffit d'utiliser un tableau de chaînes de caractères comme valeur.
Exemple ( code Java 8 ) : |
package fr.jmdoudoux.dej.junit5suite;
import org.junit.platform.runner.JUnitPlatform;
import org.junit.platform.suite.api.IncludeTags;
import org.junit.platform.suite.api.SelectPackages;
import org.junit.runner.RunWith;
@RunWith(JUnitPlatform.class)
@SelectPackages("fr.jmdoudoux.dej.junit5")
@IncludeTags({"MonTag","MonAutreTag"})
public class MaSuiteDeTests {
}
L'annotation @ExcludeTags s'utilise de manière similaire pour exclure des classes de tests de la suite.
Important : il n'est pas possible d'utiliser @IncludeTags et @ExcludeTags dans un même plan de test.
Junit 5 ne propose pas de support pour certaines fonctionnalités de JUnit 4 telles que les Rules ou les Runner car elles proposent d'autres fonctionnalités de remplacement. Cependant JUnit 5 propose un moteur d'exécution des tests dans le module JUnit Vintage qui permet d'exécuter des tests JUnit 3 ou JUnit 4.
Le module JUnit Vintage propose une implémentation de l'interface TestEngine pour exécuter des tests JUnit 3 et 4. Ceci est d'autant plus important qu'il existe un nombre gigantesque de tests automatisés écrits en utilisant JUnit 3 et 4. Il faut ajouter la dépendance junit-vintage-engine dans le classpath pour permettre l'exécution de tests JUnit 3 et 4 par la plateforme JUnit 5.
Comme les classes et interfaces de JUnit 4 et 5 sont dans des packages différents, il est possible d'avoir les jars de deux versions dans la classpath et ainsi d'avoir des tests dans les deux versions dans un même projet. Cela peut permettre de commencer à utiliser JUnit 5 dans un projet et de migrer les tests existant au fur et à mesure.
L'équipe de JUnit prévoit de livrer des versions de maintenance et de bug fixes pour JUnit 4, ce qui devrait laisser le temps de migrer les tests vers JUnit Jupiter.
Les bibliothèques existantes comme Hamcrest ou AssertJ sont toujours compatibles avec JUnit 5.
Attention JUnit 4 avait Hamcrest en dépendance et proposait l'assertion assertThat() pour utiliser ses matcher. Avec JUnit 5, il faut utiliser directement Hamcrest après avoir ajouté la dépendance dans le classpath.
Plusieurs points sont à prendre en compte pour migrer des tests JUnit 4 vers JUnit 5 :
JUnit 4 et 5 ont des annotations similaires mais pour la plupart avec des noms différents :
ROLE |
JUNIT 4 |
JUNIT 5 |
Définir une méthode comme un cas de test |
@Test |
@Test |
Exécuter la méthode annotée avant l'exécution de la première méthode de tests de la classe courante |
@BeforeClass |
@BeforeAll |
Exécuter la méthode annotée après l'exécution de toutes les méthodes de tests de la classe courante |
@AfterClass |
@AfterAll |
Exécuter la méthode annotée avant l'exécution de chaque méthode de test |
@Before |
@BeforeEach |
Exécuter la méthode annotée après l'exécution de chaque méthode de test |
@After |
@AfterEach |
Désactiver une classe ou une méthode de test |
@Ignore |
@Disabled |
Définir une fabrique pour des tests dynamiques |
@TestFactory |
|
Définir un test imbriqué |
@Nested |
|
Associer un tag à la classe ou la méthode de tests |
@Category |
@Tag |
Enregistrer une extension |
@ExtendWith |
Il existe de nombreuses différences entre JUnit 4 et JUnit 5.
JUnit 4 |
JUNIT 5 |
|
Architecture |
Un seul jar |
Composée de trois sous-projets : JUnit Platform, JUnit Jupiter et JUnit Vintage |
Version du JDK |
Java 5 ou supérieur |
Java 8 ou supérieur |
Les assertions |
La classe org.junit.Assert contient les assertions. Les surcharges qui acceptent en paramètre un message l'attendent en premier paramètre Hamcrest est fournie en dépendance de JUnit utilisable dans l'assertion assertThat() |
La classe org.junit.jupiter.Assertions contient les assertions. Elle contient notamment la méthode assertAll() pour permettre de réaliser des assertions groupées. Les surcharges qui acceptent en paramètre un message l'attendent en dernier paramètre Hamcrest n'est plus fournie en dépendance |
Les assumptions |
La classe org.junit.Assume contient les suppositions. Elle propose plusieurs méthodes : |
La classe org.junit.jupiter.Assumptions contient les suppositions. Elle propose uniquement trois méthodes qui possèdent de nombreuses surcharges : |
Les tags |
@category |
@tag |
Les suites de tests |
@RunWith et @Suite |
@RunWith, @SelectPackages et @SelectClasses |
Développons en Java v 2.40 Copyright (C) 1999-2023 Jean-Michel DOUDOUX. |