Tests unitaires en Java
Application avec Junit 5
Bruno Mermet
Octobre 2020
1 / 35
Les tests unitaires
I Les tests unitaires sont des tests permettant de tester de toutes
petites unités d’un logiciel. Ils sont notamment dédiés à des
tests de méthodes.
I Les tests unitaires doivent être lancés aussi souvent que
possibles, notamment à chaque modification du code source ;
ils faut donc qu’ils soient exécutables automatiquement. Ils
sont donc écrits dans le même langage de programmation que
le logiciel.
I Comme les tests unitaires doivent être exécutés fréquemment,
ils doivent être rapides, et notamment sans interaction avec un
humain. Par ailleurs, ils doivent être reproductibles.
2 / 35
Junit
I JUnit est une famille de bibliothèques de tests unitaires. Le
principe a été repris dans beaucoup d’autres langages pour
générer les bibliothèques faisant partie du système xUnit.
I Un peu d’histoire
I La première version de Junit largement utilisée fut Junit 3.
Cette version est désormais considérée comme obsolète.
I La version suivante de Junit fut Junit 4. C’est la version qui a
vraiment vu l’explosion de Junit. Elle n’est pas compatible avec
Junit 3. Elle reste encore la version la plus présente dans les
projets industriels.
I La dernière version de Junit est Junit 5. Elle n’est pas
compatible avec Junit 4, mais présente de nombreuses
similarités, et des méthodes particulières permettent de lancer
depuis Junit 5 des tests Junit 4. Le développement de cette
nouvelle version a pris du temps, mais elle semble avoir atteint
l’âge de la maturité.
3 / 35
Junit 5
I Référence : https://junit.org
I Javadoc : https://junit.org/junit5/docs/current/api/index.html
I Structure de base : 3 “blocs” :
I Junit Platform : le cœur du système ; cadre général
permettant d’exécuter des tests
I Junit Jupiter : la bibliothèque nécessaire à l’exécution de tests
Junit 5
I Junit Vintage : pour exécuter des tests Junit 3 et Junit 4
depuis Junit 5.
I Intégration dans les outils de développement
I Junit 5 est maintenant pris en charge par les divers IDE
(Eclipse, Netbeans, IntelliJ Idea)
I Junit 5 est pris en charge par les outils de build (Maven, Gradle)
4 / 35
Contenu du présent support
Ce que ce document est :
I un support relativement détaillé pour un cours sur les
fonctionnalités de base de Junit 5
Ce que ce document n’est pas :
I Un cours sur l’écriture des tests
I Un document autonome
I Une description exhaustive des fonctionnalités de Junit 5 car :
I cela prendrait trop de temps
I Un certain nombre de fonctionnalités de Junit 5 sont encore
expérimentales
5 / 35
Tests élémentaires : principes de base
I Structure d’une classe de test
I 1 classe de test = un ensemble de méthodes de test
I 1 classe de test par classe à tester
I 1 méthode de test = 1 cas de test
I 1 cas de test = (description, données d’entrée, résultat attendu)
I Structure d’une méthode de test de base
I méthode d’instance publique
I annotée avec @Test
I ne prend aucun paramètre
I ne renvoie rien
I lève une AssertionError en cas de test échoué
I Conventions
I nom d’une classe de test : NomClasseTestéeTest
I nom d’une méthode de test : testNomMethodeTestee
6 / 35
Premier exemple
Classe à tester Classe de test
public class Calculatrice {
import static org.junit.jupiter.api.Assertions.assertEquals;
private int x;
private int y;
import org.junit.jupiter.api.Test;
public Calculatrice(int a, int b) {
class CalculatriceTest {
x = a;
y = b;
@Test
}
void testConstucteur() {
// données d'entrées
public int ajouter() {
Calculatrice calc = new Calculatrice(4, 5);
x -= y;
// résultat attendu
return x;
String resultatAttendu = "x = 4; y = 5";
}
// résultat effectif
String resultatEffectif = calc.toString();
@Override
// vérification
public String toString() {
assertEquals(resultatAttendu, resultatEffectif,
return "x = " + x + "; y = " + y;
"construction simple");
}
}
}
}
7 / 35
Premier exemple : exécution dans Eclipse
8 / 35
Deuxième Exemple
Classe de test
Classe à tester import static org.junit.jupiter.api.Assertions.assertEquals;
public class Calculatrice {
import org.junit.jupiter.api.Test;
private int x;
private int y;
class CalculatriceTest {
public Calculatrice(int a, int b) {
@Test
x = a;
void testConstucteur() {...
y = b;
}
@Test
void testAjouter() {
public int ajouter() {
// données d'entrées
x -= y;
Calculatrice calc = new Calculatrice(4, 5);
return x;
// résultat attendu
}
int resultatAttendu = 9;
// résultat effectif
@Override
int resultatEffectif = calc.ajouter();
public String toString() {
// vérification
return "x = " + x + "; y = " + y;
assertEquals(resultatAttendu, resultatEffectif,
}
"ajout simple");
}
}
9 / 35
Deuxième exemple : exécution dans Eclipse
10 / 35
La classe Assertions
Rôle
Ensemble de méthodes statiques aidant à écrire des tests. Ces
méthodes lèvent des AssertionFailedError (sous-type de
AssertionError) en cas d’échec.
Structure des différentes méthodes
La plupart de ces méthodes existent sous 3 formats :
I avec les paramètres de base
I avec les paramètres de base et une chaîne de caractères
correspondant au message à afficher en cas d’échec
I avec les paramètres de base et un Supplier<String>,
méthode construisant la chaîne de caractères à afficher en cas
d’échec
11 / 35
Méthodes de base de la classe Assertions (1)
I assertTrue(boolean) : Le paramètre doit être vrai
I assertEquals(Object attendu, Object effectif) :
attendu.equals(effectif) doit être vrai
I assertEquals(typeDeBase attendu, typeDeBase
effectif) : attendu == effectif doit être vrai
I assertEquals(typeReel attendu, typeReel effectif,
typeReel delta) : abs(attendu - effectif) < delta
doit être vrai
I assertSame(Object attendu, Object effectif) :
attendu == effectif doit être vrai
I assertNull(Object objNul) : objNul == null doit être
vrai
12 / 35
Méthodes de base de la classe Assertions (2)
I assertArrayEquals(Type[] attendu, Type[]effectif) :
égal “profond” sur les tableaux
I assertIterableEquals(Iterable<?> attendu,
Iterable<?> effectif) : égal “profond” sur les itérables
I assertThrows(Class<T> typeException, Executable
exécutable) : exécutable doit lever une exception du type
spécifié
13 / 35
Méthodes de la classe Assertions : mais aussi. . .
I assertFalse(boolean) : No comment. . .
I assertNotEquals(...) : No comment. . .
I assertNotSame(Object attendu, Object effectif) :
No comment. . .
I assertNotNull(Object obj)
I assertArrayEquals(TypeReel[] attendu, TypeReel[]
effectif, TypeReel delta) : égal “profond” sur les tableaux
de réels
I assertDoesNotThrows(Class<T> typeException,
Executable exécutable) : No comment. . .
14 / 35
Contrôle du temps d’exécution
I assertTimeout(Duration durée, Executable
exécutable)
I l’exécutable s’exécute complètement, mais il faut que sa
durée d’exécution soit inférieure à durée pour que le test soit
considéré comme réussi
I assertTimeout(Duration durée, ThrowingSupplier<T>
exécutable)
I l’exécutable s’exécute complètement, mais il faut que sa
durée d’exécution soit inférieure à durée pour que le test soit
considéré comme réussi. Par ailleurs, s’il n’y a pas eu
d’exception levée, le résultat de exécutable est renvoyé.
I assertTimeoutPreemptively(Duration duréee,
Executable exécutable)
I assertTimeoutPreemptively(Duration duréee,
ThrowingSupplier<T> exécutable)
I Idem que précédemment, mais l’exécution est interrompue si
elle n’est pas terminée au bout du délai spécifié
15 / 35
Temps d’exécution : exemple
@Test
void testDureeExecution() {
Calculatrice calc = new Calculatrice(4,5);
int res = assertTimeout(Duration.ofMillis(1),
() -> {return calc.soustraire();});
assertEquals(-1, res);
}
16 / 35
Détecter plusieurs erreurs en une fois
Problème
Comme un échec se traduit par une levée d’exception, cela
interrompt l’exécution de la méthode de test. Du coup, si une
méthode de test fait plusieurs vérifications, Le premier échec qui
survient bloc l’exécution des tests suivants.
Solution
I assertAll(String enTete, Collection<Executable>
exécutables)
I assertAll(String enTete, Stream<Executable>
exécutables)
I assertAll(String enTete, Executable...
exécutables)
I Avec ces différentes méthodes, les AssertionError pouvant
survenir dans les différents exécutables sont rassemblées en
une seule MultipleFailuresError
17 / 35
Plusieurs erreurs à la fois : Exemple
Classe à tester
class CalculatriceTest {
@Test
void testConstucteur() {...
@Test
void testAjouter() {...
@Test
void testSuccessionOperations() {
// données d'entrées
Calculatrice calc = new Calculatrice(4, 5);
final int res1, res2;
res1 = calc.ajouter();
res2 = calc.ajouter();
assertAll("Additions Sucessives",
() -> {assertEquals(9, res1);},
() -> {assertEquals(14, res2);}
);
}
}
18 / 35
Méthode assertLinesMatch(List<String> attendu,
List<String> effectif)
Principe
Vérifier une liste de lignes. Les lignes de attendu peuvent être :
I des lignes réellement attendues
I des lignes de la forme >>entier >>, spécifiant un nombre de ligne à sauter
I des lignes de la forme >>nonEntier >>, pour préciser que l’on peut sauter
un nombre quelconque de lignes jusqu’à ce qu’une ligne soit identique à la
ligne suivante
Exemple (attendu, effectif 1, effectif 2)
a
z
a a
z
>> 2 >> z
b
b z
x
>> sans importance >> b
y
c c
z
c
19 / 35
Alternatives à la classe Assertions
Il est possible d’utiliser d’autres bibliothèques d’assertions. Il est
notamment possible d’utiliser la méthode assertThat(T,
Matcher<? super T>) de la bibliothèque Hamcrest
(org.hamcrest.core).
Exemple
@Test
void testAjouterHamcrest() {
// données d'entrées
Calculatrice calc = new Calculatrice(4, 5);
// résultat attendu
int resultatAttendu = 9;
// résultat effectif
int resultatEffectif = calc.ajouter();
// vérification
assertThat(resultatEffectif, equalTo(9));
}
20 / 35
Décorer l’exécution des tests
I Initialisation avant l’exécution de tous les tests
I Méthode de classe ne renvoyant rien et sans paramètre annotée
avec @BeforeAll
I Finalisation à la fin de l’exécution de tous les tests
I Méthode de classe ne renvoyant rien et sans paramètre annotée
avec @AfterAll
I Code à exécuter avant chaque test
I Méthode d’instance ne renvoyant rien et sans paramètre
annotée avec @BeforeEach
I Code à exécuter après chaque test
I Méthode d’instance ne renvoyant rien et sans paramètre
annotée avec @AfterEach
21 / 35
Affichage du nom des tests
Les différentes solutions
I Par défaut : le nom de la méthode
I Modifié statiquement : @DisplayName(nomAAfficher )
I Modifié dynamiquement (depuis Junit 5.4):
@DisplayNameGeneration(nomClasseGénérateur.class)
Écriture d’un générateur de nom
I écrire une classe implantant DisplayNameGenerator
(idéalement, hériter de
DisplayNameGenerator.ReplaceUnderscores ou de
DisplayNameGenerator.Standard)
I redéfinir la ou les méthodes souhaitées
(generateDisplayNameForMethod() par exemple)
I N.B. : un générateur de nom pourra souvent être défini sous la
forme d’une classe interne statique de la classe de test
22 / 35
Étiquetage des tests
Il est possible d’annoter des méthodes de test avec une ou plusieurs
étiquettes. Cela s’effectue avec l’annotation @Tag(étiquette).
Les étiquettes peuvent être utilisées dans le pom.xml d’un projet
maven pour requérir/invalider l’exécution des tests en question.
23 / 35
Suppositions (Assumptions)
Il est possible d’invalider certains tests à l’exécution en fonction du
contexte. Ces tests ne seront alors pas exécutés. Pour ce faire, on
peut utiliser les méthodes de classe définies dans la classe
Assumptions (du package org.junit.jupiter.api). Ces
méthodes ressemblent aux Méthodes de la classe Assertions, mais
elles lèvent une TestAbortedException afin que le test ne soit
pas considéré comme un échec.
Il est également possible d’invalider certains tests avec les
annotations définies dans le package
org.junit.jupiter.api.condition :
I @DisabledIfEnvironmentVariable /
@EnabledIfEnvironmentVariable
I @DisabledIfSystemProperty /
@EnabledIfSystemProperty
I @DisabledOnOs / @EnabledOnOs
I @DisabledOnjre / @EnabledOnjre
24 / 35
Injection de dépendance
Si une méthode ou un constructeur d’une classe de test contient des
paramètres de type TestInfo ou TestReporter, des instances
adéquates sont automatiquement passées en paramètre.
I TestInfo : permet de fournir des informations sur les conditions
de test, notamment grâce aux méthodes suivantes :
I getDisplayName() : String
I getTags() : Set<String>
I getTestClass() : Optional<Class<?>>
I getTestMethod() : Optional<Method>
I TestReporter : permet d’enrichir les messages renvoyés pendant
l’exécution des tests grâce aux méthodes suivantes :
I publishEntry(String) : void
I publishEntry(String clé, String valeur) : void
I publishEntry(Map<String, String>) : void
25 / 35
Exécuter plusieurs fois une méthode de test
Dans certains cas, il peut être souhaitable d’exécuter plusieurs fois une méthode
de test. Il faut alors utiliser l’annotation @RepeatedTest(nombreRépétitions) au
lieu de l’annotation @Test.
Premier Exemple
@RepeatedTest(10)
void testSoustractionNulle() {
Random dé = new Random();
int valeurTest = dé.nextInt(100);
Calculatrice calc = new Calculatrice(valeurTest, valeurTest);
int resultatEffectif = calc.soustraire();
// vérification
assertEquals(0, resultatEffectif);
}
Exemple avec nom plus détaillé
@RepeatedTest(value=10, name=RepeatedTest.LONG_DISPLAY_NAME)
void testSoustractionNulle() {...
26 / 35
Répétition de test et injection de dépendance
Dans le cas d’un test répété, la présence d’un paramètre de type
RepetitionInfo pour la méthode de test, comme pour les
méthodes AfterEach ou BeforeEach amènera Junit à passer
automatiquement l’objet en question. Sur cet objet, 2 méthodes
sont disponibles :
I getCurrentRepetition() : int
I getTotalRepetitions() : int
27 / 35
Tests paramétrés (1)
Principe
Il est possible de paramétrer des méthodes de test pour les faire
exécuter avec des valeurs différentes. Il faut alors :
I Annoter avec @ParameterizedTest au lieu de @Test
I Fournir une source pour les paramètres
Si la méthode de test prend une seul paramètre
On utilise l’annotation @ValueSource et l’on initialise le paramètre
optionnel du type requis avec un tableau de valeurs. Exemple :
@ParameterizedTest
@ValueSource(ints = {1, 3, 6, 8})
void testParametreSoustraire(int n1) {
Calculatrice calc = new Calculatrice(5, n1);
int resultatAttendu = 5 - n1;
int resultatEffectif = calc.soustraire();
assertEquals(resultatAttendu, resultatEffectif);
}
28 / 35
Tests paramétrés (2)
Si la méthode de test prend plusieurs paramètres (chaînes ou
types de base)
On peut utiliser l’annotation @CsvSource et fournir en paramètre à
cette annotation un tableau de chaînes de caractères contenant les
listes de paramètres séparés par des virgules. Exemple :
@ParameterizedTest
@CsvSource({"1, 3", "6, 8"})
void test2ParametresSoustraire(int n1, int n2) {
Calculatrice calc = new Calculatrice(n1, n2);
int resultatAttendu = n1 - n2;
int resultatEffectif = calc.soustraire();
assertEquals(resultatAttendu, resultatEffectif);
}
N.B. : l’annotation CsvFileSource permet de lire de spécifier un
fichier au format CSV comme source des données de test d’une
méthode.
29 / 35
Tests paramétrés (3)
La méthode prend un seul paramètre ; cas général
On utilise l’annotation @MethodSource à laquelle on passe en
paramètre le nom d’une méthode de la classe courante. Cette
méthode doit renvoyer un flux (Stream) de données du type de
paramètre de la méthode. La méthode de test sera appelée pour
chacun des paramètres du flux. Exemple :
@ParameterizedTest
@MethodSource("entiers")
void testParametresSoustraireViaMethode(int n2) {
Calculatrice calc = new Calculatrice(5, n2);
int resultatAttendu = 5 - n2;
int resultatEffectif = calc.soustraire();
assertEquals(resultatAttendu, resultatEffectif);
}
static IntStream entiers() {
return IntStream.of(1, 3, 5, 7);
}
30 / 35
Tests paramétrés (4)
La méthode prend plusieurs paramètres ; cas général
On utilise l’annotation @MethodSource à laquelle on passe en paramètre le
nom d’une méthode de la classe courante. Cette méthode doit renvoyer un
flux d’arguments (Stream<Arguments>). Chaque Arguments est
construit grâce à la méthode of de l’interface Arguments. Exemple :
@ParameterizedTest
@MethodSource("couplesEntiers")
void testParametresSoustraireViaMethode2(int n1, int n2) {
Calculatrice calc = new Calculatrice(n1, n2);
int resultatAttendu = n1 - n2;
int resultatEffectif = calc.soustraire();
assertEquals(resultatAttendu, resultatEffectif);
}
static Stream<Arguments> couplesEntiers() {
return Stream.of(Arguments.of(10, 3), Arguments.of(5, 9));
}
N.B. : l’annotation @ArgumentsSource permet de spécifier une classe
implantant l’interface ArgumentsProvider plutôt qu’une méthode
comme source des données.
31 / 35
Tests dynamiques
Il est possible de générer dynamiquement des tests en utilisant une
méthode génératrice de tests. Une telle méthode doit être annotée
avec @TestFactory est doit renvoyer un Iterable, un Iterateur ou
un flux de tests dynamiques (éventuellement hiérarchisés). Exemple :
@TestFactory
Stream<DynamicTest> generateurTests() {
IntStream fluxEntier = IntStream.range(0, 10);
Stream<DynamicTest> fluxTests = fluxEntier.mapToObj(
n -> dynamicTest("test " + n,
() -> {Calculatrice calc = new Calculatrice(n, n);
assertEquals(0, calc.soustraire());
}));
return fluxTests;
}
32 / 35
Configuration de Maven
Version de Jupiter >= 5.5.1
Version de java >= 8 : <dependency>
<groupId>org.junit.jupiter</groupId>
<properties>
<maven.compiler.source>1.10</maven.compiler.source> <artifactId>junit-jupiter-api</artifactId>
<maven.compiler.target>1.10</maven.compiler.target> <version>5.7.0</version>
</properties> <scope>test</scope>
</dependency>
Plugins surefire et failsafe >= <dependency>
<groupId>org.junit.jupiter</groupId>
2.22.2 <artifactId>junit-jupiter-engine</artifactId>
<version>5.7.0</version>
<plugin>
<artifactId>maven-surefire-plugin</artifactId> <scope>test</scope>
<version>2.22.2</version> </dependency>
</plugin> <dependency>
<plugin> <groupId>org.junit.jupiter</groupId>
<artifactId>maven-failsafe-plugin</artifactId> <artifactId>junit-jupiter-params</artifactId>
<version>2.22.2</version> <version>5.7.0</version>
</plugin> <scope>test</scope>
</dependency>
33 / 35
Utilisation des étiquettes depuis le pom.xml
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.0.0-M5</version>
<configuration>
<groups>tag1,tag2,TagExpr</groups>
<excludedGroups>tag3,TagExpr2</excludedGroups>
</configuration>
</plugin>
Avec :
TagExpr : Tag
(TagExpr)
!TagExpr
TagExpr | TagExpr
TagExpr & TagExpr
34 / 35
Utilisation de JUnit5 en ligne de commande
Récupérer l’archive du ConsoleLauncher
junit-platform-console-standalone
depuis
https://repo1.maven.org/maven2/org/junit/platform/junit-platform-console-standalone/
Lancer les tests avec :
java -jar ConsoleLauncher --scan-classpath -cp
repClassesDeTest -cp repClassesAppli
35 / 35