# JUnit 5 Jupiter Quick Start Tutoriel

JUnit 5 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 (comme assertEquals) 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):

<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>
1
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, la dépendance junit-jupiter-api est suffisante 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. 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>
1
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 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>
1
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.

# En utilisant le plugin Failsafe

JUnit 5 est tout aussi bien intégré avec le plugin Maven Failsafe 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>
1
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.

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

}
1
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');
}
1
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 ou hamcrest.

# 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");
}
1
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));
}
1
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());
}
1
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()));
}
1
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");
}
1
2
3
@AfterEach
public void cleanTest() {
  LOG.debug("Called after each test");
}
1
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");
}
1
2
3
@AfterAll
public static void tearDown() {
  LOG.debug("Called once after all test");
}
1
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).
1
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));
}
1
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());
}
1
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");
}
1
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));
}
1
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.

Dernière mise à jour: 7/1/2019, 4:08:10 PM