# JUnit 5 Jupiter Quick Start Tutoriel
JUnit 5 (opens new window) est la première version de JUnit à séparer l'API pour découvrir et exécuter les tests (appelée JUnit Platform) de l'API pour écrire les tests (appelée JUnit Jupiter)
- JUnit Jupiter est l'API utilisée par les développeurs pour écrire les tests unitaires.
Cette API contient les annotations (comme
@Test
) et les assertions (commeassertEquals
) appelée le programming model mais aussi une API pour les extensions appelée the extension model. - JUnit Platform contient à la fois:
- l'API Launcher utilisée par les IDEs et les outils de build (comme graddle et maven) pour découvrir et exécuter les tests et
- l'API Test Engine utilisée par des librairies tierces (type JUnit Jupiter, Cucumber, FitNesse et même JUnit 4) pour permettre à leurs tests de s'exécuter sur cette plateforme.
En implémentant cette séparation des responsabilités, l'équipe de JUnit a réalisé un travail assez impressionnant sans pour autant compliquer l'usage pour le développeur (et même en ajoutant pas mal de fonctionnalités au passage \o/).
# Installation avec Maven
Pour commencer, il faut inclure JUnit Jupiter Engine comme dépendance de test dans la configuration maven (pensez à vérifier la dernière version sur mvnrepository (opens new window)):
<dependencies>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-engine</artifactId>
<version>5.4.2</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
<version>5.4.2</version>
<scope>test</scope>
</dependency>
</dependencies>
2
3
4
5
6
7
8
9
10
11
12
13
14
Cette dépendance inclut à la fois JUnit Jupiter Engine et une implémentation de la plateforme JUnit.
Avec cette dépendance, on peut écrire des classes de test, annoter leurs méthodes avec @Test
et utiliser
les assertions et assumptions de Jupiter.
TIP
Avec IntelliJ IDEA (opens new window), la dépendance junit-jupiter-api est suffisante (opens new window) pour exécuter des tests à partir d'IDEA. IDEA déduit automagiquement à partir de cette dépendance la plateforme JUnit (launcher et test engines) requises pour découvrir et exécuter les tests.
# Exécuter ces tests avec Maven
# En utilisant le plugin Surefire
JUnit 5 est maintenant bien intégré avec le plugin Maven Surefire (opens new window).
A partir de la version 2.22.0, aucune configuration spécifique n'est requise pour exécuter des tests JUnit Jupiter avec mvn test
.
Il suffit d'ajouter Surefire dans la section build de maven:
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>2.22.0</version>
</plugin>
</plugins>
</build>
2
3
4
5
6
7
8
9
En revanche, avant la version 2.22.0, la glue entre le plugin Surefire et la plateforme JUnit
n'était pas encore intégrée côté plugin. C'est pourquoi il faut ajouter la dépendance junit-platform-surefire-provider
avec les versions < 2.22.0 (ici le JIRA de la donation de code (opens new window) entre les deux projets):
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>2.21.0</version>
<dependencies>
<dependency>
<groupId>org.junit.platform</groupId>
<artifactId>junit-platform-surefire-provider</artifactId>
<version>1.2.0</version>
</dependency>
</dependencies>
</plugin>
</plugins>
</build>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Par défaut, toutes les classes dont le nom match Test*
, *Test
, *Tests
or *TestCase
sont incluses.
Pour les cas où la convention de nommage des classes de test est différente, ce comportement
est configurable (opens new window).
# En utilisant le plugin Failsafe
JUnit 5 est tout aussi bien intégré avec le plugin Maven Failsafe (opens new window) pour l'exécution de tests d'intégration. Là encore, il suffit d'ajouter ce plugin dans la section build de maven:
<build>
<plugins>
<plugin>
<artifactId>maven-failsafe-plugin</artifactId>
<version>2.22.0</version>
</plugin>
</plugins>
</build>
2
3
4
5
6
7
8
Par défaut, toutes les classes dont le nom match IT*
, *IT
or *ITCase
seront incluses.
Ce comportement est là encore configurable (opens new window).
# Ecrire des tests
# Première classe de test
Pour écrire une classe de test, il suffit de s'assurer que son nom match le pattern d'inclusion
de l'exécuteur de test (surefire, failsafe, ...). A partir de là, toutes les méthodes de cette classe
annotées avec org.junit.jupiter.api.Test
feront parties des tests:
package com.piecesofcode.quickstart.junit5;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertTrue;
public class SomethingToTest {
@Test
public void dummyTest() {
assertTrue(true);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
# DisplayName
Jupiter introduit l'annotation @DisplayName
pour aider les développeurs à mieux décrire leurs tests:
@Test
@DisplayName("Showcase boolean assertion on USA country name ☺")
public void poorTestName() {
assertTrue(Country.USA.getName().matches("U.*S.*A.*"));
assertFalse(() -> Country.USA.getName().isEmpty(), "Java 8 lambda can be used ");
System.out.println('\u263A');
}
2
3
4
5
6
TIP
Avec l'annotation @DisplayName
, le nom du test affiché dans le rapport n'est plus celui de la méthode
(comme c'était le cas avec JUnit 4) mais bien la chaîne de caractère passée en paramètre de l'annotation.
Il devient donc possible d'utiliser des espaces et des caractères spéciaux
(et si ça ne suffit pas, même des emoji 😮)
# Assertions
Jupiter intègre plusieurs méthodes d'assertion pour exprimer ces cas de test. Ce sont toutes
des méthodes statiques de la classe org.junit.jupiter.api.Assertions
qui peuvent s'importer via
un import static
.
TIP
Les assertions de Jupiter peuvent être remplacées par une celles d'une autre librairie comme assertj (opens new window) ou hamcrest (opens new window).
# Assertion booléenne
@Test
public void booleanAssertion() {
assertTrue(Country.USA.getName().matches("U.*S.*A.*"));
assertFalse(() -> Country.USA.getName().isEmpty(), "Java 8 lambda can be used");
}
2
3
4
# Assertion d'égalité
@Test
public void equalityAssertion() {
assertEquals(UNITED_STATES_OF_AMERICA, Country.USA.getName());
assertEquals(
UNITED_STATES_OF_AMERICA,
Country.USA.getName(),
"Assertion can display a message when they fail.");
assertEquals(36.0f, Country.USA.getDensity(), 0.1f);
Lists.list(Country.USA, Country.CHINA, Country.AUSTRALIA, Country.FRANCE)
.stream()
.map(Country::getName)
.forEach(countryName -> assertFalse(countryName::isEmpty));
}
2
3
4
5
6
7
8
9
10
11
12
TIP
Lorsqu'on teste l'égalité entre float, il ne faut pas oublier d'indiquer un delta pour tenir compte de l'imprécision des nombres à virgule flottante.
# Assertion d'une exception
Il est également possible d'attendre une exception avec assertThrows
.
Dans le code ci-dessous, assertThrows
retourne l'exception attendue, ce qui permet ensuite de
faire des assertions sur ces propriétés.
@Test
public void assertionOnExceptions() {
Country noCountryForOldMan = null;
Throwable exception =
assertThrows(NullPointerException.class, () -> noCountryForOldMan.getPopulation());
assertNull(exception.getCause());
}
2
3
4
5
6
# Grouper les assertions
Lorsqu'une méthode de test contient plusieurs assertions, le test échoue à la première assertion fausse,
sans exécuter les assertions suivantes. Si l'on souhaite obtenir le résultat de toutes les assertions,
même lorsque l'une d'entre elle échoue, il est possible d'utiliser assertAll
. assertAll
échoue
si l'une de ses assertions échoue mais exécute dans tous les cas l'ensemble des assertions. Celà
permet d'avoir un rapport sur l'ensemble des échecs:
@Test
public void assertionGroup() {
assertAll(
() -> assertEquals(UNITED_STATES_OF_AMERICA, Country.USA.getName()),
() -> assertEquals("Washington, D.C.", Country.USA.getCapital()));
}
2
3
4
5
# Lifecycle Annotations
Les annotations @BeforeEach
et @AferEach
sont utilisées pour exécuter du code avant (resp. après)
chaque méthode de test.
@BeforeEach
public void initTest() {
LOG.debug("Called before each test");
}
2
3
@AfterEach
public void cleanTest() {
LOG.debug("Called after each test");
}
2
3
De même, @BeforeAll
and @AfterAll
sont utilisées pour exécuter du code avant (resp. après)
toutes les méthodes de test (et donc, seulement une fois par classe de test).
@BeforeAll
public static void setUp() {
LOG.debug("Called once before all tests");
}
2
3
@AfterAll
public static void tearDown() {
LOG.debug("Called once after all test");
}
2
3
WARNING
Notes que @BeforeAll
et @AfterAll
sont des méthodes static. Dans le cas contraire
on obtient cette exception:
org.junit.platform.commons.JUnitException: @BeforeAll method 'public void ...'
must be static unless the test class is annotated with
@TestInstance(Lifecycle.PER_CLASS).
2
3
C'est parce que, par défaut, JUnit instancie la classe de test pour chaque méthode de test.
Ces méthodes sont donc statiques pour qu'elles puissent partager des données entre les tests.
Ce comportement se change en ajoutant l'annotation @TestInstance(Lifecycle.PER_CLASS)
à la classe de test.
# Test conditionnels
Parfois les tests doivent être exécutés seulement sous certaines conditions. Jupiter propose 2 méthodes pour gérer ça: dans le code via les assumptions ou de manière déclarative via les annotations et l'extension model.
# Test conditionnels via les assumptions
Les assumptions permettent de définir dans le code les conditions d'exécution des tests. Elles se présentent
sous la forme de méthodes statiques définies dans la classe org.junit.jupiter.api.Assumptions
et peuvent donc s'importer via un import static
.
@Test
public void assumption() {
CountryProvider provider = new GeoNameCountryProvider();
Countries countries = provider.provides();
assumeTrue(countries != null && !countries.asList().isEmpty());
assertTrue(countries.asList().contains(Country.FRANCE));
}
2
3
4
5
6
Dans l'exemple ci-dessus, si le provider ne retourne pas de countries, le test est reporté comme aborted et non en erreur. Par contre, si le provider retourne bien des countries, le test continue son exécution normalement en exécutant les assertions qui suivent.
Il est également possible de n'exécuter qu'une partie d'un test en fonction d'une assumption donnée
via assumeThat
. Dans l'exemple ci-dessous, assertEquals(UNITED_STATES_OF_AMERICA, Country.USA.getName());
serra toujours exécuté, mais assertTrue(countries.asList().contains(Country.USA));
ne serra exécuté
que si le provider retourne bien des countries:
@Test
public void partialAssumption() {
CountryProvider provider = new GeoNameCountryProvider();
Countries countries = provider.provides();
assumingThat(
countries != null && !countries.asList().isEmpty(),
() -> assertTrue(countries.asList().contains(Country.USA)));
assertEquals(UNITED_STATES_OF_AMERICA, Country.USA.getName());
}
2
3
4
5
6
7
8
# Test conditionnels via les annotations
Jupiter fournit également plusieurs annotations pour activer/désactiver des tests sous
certaines conditions (disponible dans le package org.junit.jupiter.api.condition
). La plus simple
de ces annotations est @Disable
qui permet de désactiver un test.
@Test
@Disabled
public void disabledTest() {
LOG.debug("This code is not executed");
}
2
3
4
Les autre conditions intégrées inclus:
- Os:
@EnabledOnOs
ou@DisabledOnOs
active/désactive les tests en fonction de l'OS sur lequel ils tournent. - Jre (java version):
@EnabledOnJre
ou@DisabledOnJre
active/désactive les tests en fonction de la version du JRE sur lequel ils tournent. - System Property:
@EnabledIfSystemProperty
ou@DisabledIfSystemProperty
active/désactive les tests en fonction de la valeur d'une propriété système. - Environment Variable:
@EnabledIfEnvironmentVariable
ou@EnabledIfEnvironmentVariable
active/désactive les tests en fonction d'une variable d'environnement.
Toutes ces annotations peuvent être placées au niveau de la classe de test ou de la méthode de test qui doit être exécutée conditionnellement.
@Test
@EnabledIfSystemProperty(named = "user.language", matches = ".*en.*")
public void testOnlyIfUserLanguageIsEnglish() {
System.getProperties().forEach((key, value) -> LOG.debug("{}:{}", key, value));
}
2
3
4
TIP
@EnabledIfSystemProperty
et @EnabledIfEnvironmentVariable
prennent 2 paramètres,
named
pour le nom de la propriété système (resp. variable d'environnement) et matches
pour sa valeur.
Ce second paramètre est évalué comme une regex par Jupiter, ce qui permet d'utiliser des wilcards.