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>

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
	}
}

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

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

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
Lancer un 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();
		}
	}
}

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

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.