Sommaire
Présentation
JUnit est un framework permettant l'écriture et l'exécution de tests automatisés. Il a été développé en 2005 et la dernière version du framework historique JUnit4 a été publiée en 2019. JUnit4 est compatible avec toutes les versions de java depuis le jdk1.5
JUnit5 a été publié en 2017 et intègre les spécificités java8 (par exemple les lambda) et apporte un certain nombre de compléments :
- les tests imbriqués
- les tests dynamiques
- les tests paramétrés qui offrent différentes sources de données
JUnit5 n'est pas une nouvelle version de JUnit4 mais un nouveau Framework qui a été entièrement redéveloppé et utilise des classes et des annotations différentes de son prédécésseur. Le fonctionnement global restant similaire.
Il existe un projet permettant d'exécuter des tests écrits avec JUnit3 ou 4 dans l'environnement JUnit5 : org.junit.vintage, son utilisation est très simple et ne sera pas étudiée lors de cette formation
Mise en place
Intégration du framework au projet
Pour intégrer JUnit à un projet existant, il faut ajouter la dépendance à JUnit dans le pom.xml du projet :
<!-- https://mvnrepository.com/artifact/org.junit.jupiter/junit-jupiter-api -->
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
<version>5.8.1</version>
<scope>test</scope>
</dependency>
<!-- https://mvnrepository.com/artifact/junit/junit -->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.13.2</version>
<scope>test</scope>
</dependency>
Il est également possible d'ajouter le jar au build path du projet.
Organisation du projet et des tests
Pour chaque classe que nous souhaitons tester, il faut créer une nouvelle classe de test.
Par convention, si nous voulons tester la classe Traitement qui se trouve dans le source folder "src/main/java" : package fr.julien.formation, nous allons créer :
- un second source folder "src/test/java"
- le même package fr.julien.formation dans le nouveau source folder
- La classe test sera dans ce package et s'appelera TraitementTest
formation/
|-- src/main/java/
| `-- fr.julien.formation/
| `-- Traitement.java
|-- src/main/resources/
|-- src/test/java/
| `-- fr.julien.formation/
| `-- TraitementTest.java
`-- src/test/resources/
Création d'un test
Création d'un test simple
Pour indiquer qu'une méthode correspond à un test, il suffit d'utiliser l'annotation @Test
package fr.julien.formation;
import org.junit.jupiter.api.Test;
public class MonTest {
@Test
void testSimple() {
// Code exécuté pour jouer le test
}
}
package fr.julien.formation;
import org.junit.Test;
public class MonTest {
@Test
void testSimple() {
// Code exécuté pour jouer le test
}
}
Les assersions
Pour valider les résulats, nous allons utiliser la classe Assertions (ou assert en JUnit4) qui permet de réaliser des contrôles. Si un des contrôles effectués par assert échoue, le test échoue
- assertEquals(int a, int b)
-
Le contrôle est correct si a est égal à b (a==b), pour cette méthode, les types primitifs sont utilisés
- assertEquals(Object a, Object b)
-
Le contrôle est correct si a est égal à b (a.equals(b)), il est alors important de penser s'il y a lieu à surcharger la méthode équals
- assertTrue(boolean a)
-
Le contrôle est correct si la variable a est true, généralement utilisé de la manière suivante : Assert.assertTrue(variable.equals(variable2))
- assertFalse(boolean a)
-
Le contrôle est correct si la variable a est false, généralement utilisé de la manière suivante : Assert.assertFalse(variable.equals(variable2))
- fail()
-
L'exécution de cette instruction entraîne un échec du test
package fr.julien.formation;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.Assertions;
public class MonTest {
@Test
void testSimple() {
String s="texte";
Assertions.assertEquals("texte", s);
}
}
package fr.julien.formation;
import org.junit.Test;
import org.junit.Assert;
public class MonTest {
@Test
void testSimple() {
String s="texte";
Assert.assertEquals("texte", s);
}
}
Cycle de vie des tests
Afin de mettre en place l'environnement d'exécution des tests, il est possible d'écrire des méthodes qui sont exécutées avant et après le test. Ces méthodes sont annotées grâce aux annotations définies ci-dessous
- @BeforeEach (ou @Before en JUnit 4)
-
Méthode exécutée avant chaque méthode préfixée par @Test
- @BeforeAll (ou @BeforeClass en JUnit 4)
-
Méthode exécutée une fois avant l'exécution de la classe de test
- @AfterEach (ou @After en JUnit 4)
-
Méthode exécutée après chaque méthode préfixée par @Test
- @AfterAll (ou @AfterClass en JUnit 4)
-
Méthode exécutée une fois après l'exécution de la classe de test
package fr.julien.formation;
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
public static void debut() {
System.out.println("Début des tests");
}
@AfterAll
public static void fin() {
System.out.println("Fin des tests");
}
@BeforeEach
public void setUp() {
System.out.println("Début du test");
}
@AfterEach
public void tearDown() {
System.out.println("Fin du test");
}
@Test
void testSimple() {
String s="texte";
Assert.assertEquals("texte", s);
}
}
package fr.julien.formation;
import org.junit.Test;
import org.junit.After;
import org.junit.AfterClass;
import org.junit.Assert;
import org.junit.Before;
import org.junit.BeforeClass;
public class MonTest {
@BeforeClass
public static void debut() {
System.out.println("Début des tests");
}
@AfterClass
public static void fin() {
System.out.println("Fin des tests");
}
@Before
public void setUp() {
System.out.println("Début du test");
}
@After
public void tearDown() {
System.out.println("Fin du test");
}
@Test
void testSimple() {
String s="texte";
Assert.assertEquals("texte", s);
}
}
Résultat affiché :
Début des tests
Début du test
Fin du test
Fin des tests
Lancement du test
Pour lancer un test avec eclipse :
- Clic droit sur le test à lancer (ou sur la package)
- Run As
- JUnit Test

Exemple
Nous allons tester la classe Traitement qui possède une fonction compterLettres() qui permet de retourner le nombre de lettres d'une chaine de caractères :
package fr.julien.formation;
import java.util.regex.Pattern;
public class Traitement {
private static final Pattern PATTERN_NON_LETTRES = Pattern.compile("[^A-Za-z]");
public int compterLettres(String entree) {
return PATTERN_NON_LETTRES.matcher(entree).replaceAll("").length();
}
}
Créer un test unitaire revient à créer une classe qui appelle la méthode compterLettres et qui valide le résultat du comptage pour certains cas. Nous allons créer le test qui nous assure que le comptage retourne :
- 0 pour la chaine "123"
- 5 pour la chaine "azert"
- 2 pour la chaine "01az23"
package fr.julien.formation;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
public class TraitementTest {
@BeforeEach
public void avant() {
System.out.println("Début du test");
}
@AfterEach
public void apres() {
System.out.println("Fin du test");
}
@Test
public void testerComptage() {
Traitement t = new Traitement();
try {
Assertions.assertEquals(0, t.compterLettres("012"));
Assertions.assertEquals(5, t.compterLettres("azert"));
Assertions.assertEquals(2, t.compterLettres("01az23"));
}
catch (Exception e) {
e.printStackTrace();
Assertions.fail();
}
}
}
package fr.julien.formation;
import org.junit.Before;
import org.junit.After;
import org.junit.Assert;
import org.junit.Test;
public class TraitementTest {
@Before
public void avant(){
System.out.println("Début du test");
}
@After
public void apres(){
System.out.println("Fin du test");
}
@Test
public void testerComptage() {
Traitement t = new Traitement();
try {
Assert.assertEquals(0, t.compterLettres("012"));
Assert.assertEquals(5, t.compterLettres("azert"));
Assert.assertEquals(2, t.compterLettres("01az23"));
}
catch (Exception e) {
e.printStackTrace();
Assert.fail();
}
}
}
Les suppositions
Les supposisions permettent de conditionner l'exécution d'une partie d'un test à certaines conditions. Ces opérations sont pratiques afin de vérifier des propriétés du système ou bien de s'assurer qu'un service est disponnible avant de l'interroger
- assumeTrue(boolean)
-
Suppose que le booléen passé en paramètre est True et interrompt sans erreur le test si le booléean passé en paramètre de la fonction est False
- assumeFalse(boolean)
-
Suppose que le booléen passé en paramètre est True et interrompt sans erreur le test si le booléean passé en paramètre de la fonction est True
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Assumptions;
import org.junit.jupiter.api.Test;
public class TestSupposition {
@Test
public void lireFichier() {
final File fichier = new File("fichier.txt");
Assumptions.assumeTrue(fichier.exists());
try (FileInputStream fis = new FileInputStream(fichier)) {
final byte[] lBytes = new byte[16];
fis.read(lBytes);
Assertions.assertArrayEquals("Test".getBytes(), lBytes);
}
catch (IOException e) {
Assertions.fail();
}
}
}
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import org.junit.Assert;
import org.junit.Assume;
import org.junit.Test;
public class TestSupposition {
@Test
public void lireFichier() {
final File fichier = new File("fichier.txt");
Assume.assumeTrue(fichier.exists());
try(FileInputStream fis = new FileInputStream(fichier)){
final byte[] lBytes = new byte[16];
fis.read(lBytes);
Assert.assertArrayEquals("Test".getBytes(), lBytes);
}
catch(IOException e){
Assert.fail();
}
}
}
Désactiver un test
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.
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
public class DescativationTest {
@Test
@Disabled("A écrire plus tard")
void monTest() {
fail("Non implémenté");
}
}
Tests répétés et tests paramérés
Tests répété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.
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.RepeatedTest;
public class RepetitionTest {
@DisplayName("test addition repété")
@RepeatedTest(3)
void testRepete() {
Assertions.assertEquals(2, 1 + 1, "Valeur obtenue erronée");
}
Tests paramétrés
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.
Pour utiliser les tests paramétrés, il faut ajouter la dépendance junit-jupiter-params.
<!-- https://mvnrepository.com/artifact/org.junit.jupiter/junit-jupiter-params -->
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-params</artifactId>
<version>5.8.1</version>
<scope>test</scope>
</dependency>
- @ValueSource
-
Une source de données simple sous la forme d'un tableau de chaînes de caractères ou de primitifs (ints, longs, doubles ou strings). Elle est fournie de la manière suivante : @ValueSource(strings = { "a", "b" })
import org.junit.jupiter.api.Assertions; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.ValueSource; public class MaClasseTest { @ParameterizedTest @ValueSource(ints = { 1, 2, 3 }) public void testParametreAvecValueSource(int valeur) { Assertions.assertEquals(valeur + valeur, valeur * 2); } }
- @EnumSource
-
Une source de données simple sous la forme d'une énumération. Ecrite @EnumSource(Enumeration.class)
import java.time.Month; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.EnumSource; public class MaClasseTest { @ParameterizedTest @EnumSource(Month.class) public void testParametreAvecEnumSource(Month mois) { System.out.println(mois); Assertions.assertNotNull(mois); } }
- @MethodSource
-
Une source de données dont les valeurs sont fournies par une méthode
import java.util.stream.Stream; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.MethodSource; public class MaClasseTest { @ParameterizedTest @MethodSource("fournirDonnees") public void testExecuter(String element) { Assertions.assertTrue(element.startsWith("elem")); } static Stream<String> fournirDonnees() { return Stream.of("elem1", "elem2"); } }
- @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
import org.junit.jupiter.api.Assertions; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.CsvSource; public class MaClasseTest { @ParameterizedTest() @CsvSource({ "1, 1", "1, 2", "2, 3" }) public void testAdditioner(int a, int b) { int attendu = a + b; Assertions.assertEquals(attendu, a + b); } }
- @CsvSourceFile
-
Une source de données dont les valeurs sont fournies sous la forme d'un ou plusieurs fichiers CSV
import static org.junit.jupiter.api.Assertions.assertTrue; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.CsvFileSource; public class MaClasseTest { @ParameterizedTest() @CsvFileSource(resources = "additionner_source.csv") public void testAdditionner(int a, int b) { int attendu = a + b; Assertions.assertEquals(attendu, a + b); } }
Quelques recommandations
Ecrire des tests simples
Les test unitaires doivent être simples à comprendre, à exécuter, à modififier, ainsi les test unitaires doivent respecter les contraintes suivantes
- Les tests unitaire ne contiennent pas de javadoc ni de documentation, s'il y a des points complexes c'est que le test unitaire est mal écrit
- Il n'y a pas de conception à mettre en place lorsqu'on créé des tests unitaires : on n'utilise pas d'héritage entre les tests par exemple. De manière générale, une classe de test ne référence jamais une autre classe de test
- Il est possible de créer des services permettant de faciliter l'écriture des tests, mais le service ne sera pas lui même un test
Ecrire des tests rejouables
Le test n'a pas une durée de vie limitée, il doit pouvoir être joué indéfiniement. C'est un point souvent négligé mais il faut penser que le test doit être valable quel que soit la date ou le jour d'exécution.
Une erreur fréquente est la suivante : la méthode AccesCalendrier.getAnneeCourante permet de récupérer l'année courante :
package fr.julien.formation;
import java.text.SimpleDateFormat;
import java.util.Date;
public class AccesCalendrier {
private static AccesCalendrier instance = new AccesCalendrier();
private SimpleDateFormat formatAnnee = new SimpleDateFormat("yyyy");
private AccesCalendrier() {
super();
}
public int getAnneeCourante() {
return Integer.parseInt(formatAnnee.format(new Date()));
}
public static AccesCalendrier getInstance() {
return instance;
}
}
Il est tout à fait possible de créer un test unitaire de la manière suivante
package fr.julien.formation;
import org.junit.Assert;
import org.junit.Test;
public class AccesCalendrierTest {
@Test
public void testerGetAnneeCourante() {
Assert.assertEquals(2017, AccesCalendrier.getInstance().getAnneeCourante());
}
}
Problème : l'an prochain, notre test unitaire sera en échec alors que le programme ne présentera pas d'anomalie
Dans ce cas, que faire ? Il existe plusieurs solutions
- La première est préférable mais n'est pas forcément réalisable : il s'agit d'adapter la conception de notre application à la problématique de test. Dans le cas présent, il ne faudrait plus se baser sur la date système mais sur une date passée en paramètre au lancement de java
- La seconde (toujours réalisable) consiste à modifier le test de manière a être moins restrictif ou à obtenir l'information d'une autre manière : soit récupérer la date à l'aide de Date() soit tester uniquement que l'année courante est supérieure à 2016 serait déjà préférable
- Utiliser un proxy permettant de récupérer la date et simuler ce proxy dans les tests unitaires.