Tests unitaires avec JUnit 5

Prise en main

Quelques liens utiles :
Site officiel Junit 5
User guide
API documentation

On va utiliser une classe d'exemple pour vérifier que Junit5 est opérationnel :
import static org.junit.jupiter.api.Assertions.assertEquals;

import org.junit.jupiter.api.Test;

class FirstJUnit5Tests {

    @Test
    void myFirstTest() {
        assertEquals(2, 1 + 1);
    }

}
(voir exemples/java/junit5/first)

En mode console

On peut exécuter JUnit5 en ligne de commande en utilisant junit-platform-console-standalone (version utilisée : 1.3.1).
Télécharger junit-platform-console-standalone-1.3.1.jar et le stocker en local.
(une copie se trouve sur le répêrtoire bin/ de ce cours - accessible depuis la page d'accueil du cours).

Pour avoir la liste des options à passer à junit :
java -jar /path/to/junit-platform-console-standalone-1.3.1.jar -h
ou encore plus simple
java -jar /path/to/junit-platform-console-standalone-1.3.1.jar
En se plaçant dans le répertoire contenant FirstJUnit5Tests.java :
Pour compiler :
javac -cp /path/to/junit-platform-console-standalone-1.3.1.jar FirstJUnit5Tests.java
Pour exécuter la classe de test :
java -jar /path/to/bin/junit-platform-console-standalone-1.3.1.jar -cp . -c FirstJUnit5Tests
(noter que les options cp et c sont ici des options passées à juint, pas des options de java).

Avec Eclipse

TestCase : tester une classe

Par convention, les tests unitaires sont situés dans des classes dont le nom finit par Test (par exemple, les tests de la classe MyClass seront dans MyClassTest).
Par convention, les méthodes de test commencent par test, par exemple testMyMethod().

Les méthodes de test sont repérées par des annotations (voir la liste complète).
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.junit.platform.runner.JUnitPlatform;
 
public class MyClassTest {

    @BeforeAll
    static void setup() {
        // exécutée une fois avant tous les tests
    }
 
    @BeforeEach
    void init() {
        // exécutée avant chaque méthode marquée @Test
    }
 
    @DisplayName("Test myMethod réussi")
    @Test
    void testMyMethod() {
        // Méthode de test unitaire
        // @DisplayName permet de personnaliser 
        // le message qui apparaît à l'exécution des tests
    }
 
    @AfterEach
    void tearDown() {
        // exécutée après chaque méthode marquée @Test
    }
 
    @AfterAll
    static void done() {
        // exécutée une fois après tous les tests
    }
 
}

Initialisations et fixtures

Les annotations @BeforeAll et @AfterAll servent à identifier des méthodes qui seront exécutées une seule fois avant et après tous les tests ; utile par exemple pour initialiser une connection réseau ou à une base de données.

@BeforeEach et @AfterEach identifient des méthodes qui seront exécutées avant / après chaque tests. Servent par exemple à initialiser des structures de données (fixtures) et garantir que chaque méthode de test disposera d'un état identique.
Par exemple, si on veut tester l'implémentation d'une liste, on peut créer une liste vide, qui sera utilisable par toutes les méthodes de test.

Méthodes de test

Les méthodes marquées @Test (voir la liste complète dans la doc) sont les méthodes qui vont être exécutées par junit et dont le résultat va être inclus dans le rapport.

Ces méthodes contiennent des assertions qui doivent être vérifiées pour que le test soit valide.
Ces assertions sont exprimées avec des méthodes commençant par Assert de la classe org.junit.jupiter.api.Assertions.

Par exemple, si on a écrit l'implémentation d'une liste de String contenant ces méthodes publiques :
public class CustomList{

    /** Renvoie le nombre d'élément dans la liste **/
    public int getLength(){ ... }
    
    /** Ajoute u élément à la fin de la liste **/
    public void add(String s){ ... }
    
    /** Supprime un élément de la liste **/
    public String remove(int index){ ... }
    
    /** Renvoie un élément de la liste **/
    public String getElement(int index){ ... }
}
On pourrait avoir la classe de test :
import org.junit.jupiter.api.*;            // pour les annotations
import org.junit.jupiter.api.Assertions.*; // pour les méthodes assert*() 

public class CustomListTest{

    private CustomList l1;
    
    @BeforeEach
    void init() {
        l1 = new CustomList();
    }
    
    @Test
    void testListeVide(){
        assertEquals(l1.getLength(), 0);
    }

    @Test
    void testAdd(){
        String str = "test";
        l1.add(str);
        assertEquals(l1.getLength(), 1);
        assertEquals(l1.get(0), str);
    }

}

Tester les exceptions

Il existe des méthodes org.junit.jupiter.api.Assertions.assertThrows, mais demande d'avoir vu les lambda expressions.
On peut cependant tester qu'une exception a bien été lancée en utilisant org.junit.jupiter.api.Assertions.fail()
Par exemple, l'implémentation de CustomList.remove() doit renvoyer une java.lang.IndexOutOfBoundsException si on essaye de supprimer un élément inexistant.
Dans CustomListTest, on pourrait avoir une méthode
    @Test
    void testRemove(){
        try{
            l1.remove(0);
            // une IndexOutOfBoundsException devrait être lancée par la ligne précédente
            // donc si fail() est exécuté, c'est que remove() ne s'est pas comporté comme attendu.
            fail("Opération remove() effectuée sur un index inexistant");
        }
        catch(IndexOutOfBoundsException e){
            // test réussi
        }
    }
Ou alors :
    @Test
    void testRemove(){
        try{
            l1.remove(0);
            // une IndexOutOfBoundsException devrait être lancée par la ligne précédente
            // donc si fail() est exécuté, c'est que remove() ne s'est pas comporté comme attendu.
            fail("Opération remove() effectuée sur un index inexistant");
        }
        catch(Exception e){
            assertTrue(e instanceof IndexOutOfBoundsException);
        }
    }

Conseils

Place des tests

Il est conseillé de séparer le code de test du code de production (car le code de test n'a pas besoin d'être déployé en production).
Mais on a souvent besoin de mettre le code de test dans le même package que le code de production car cela permet d'avoir accès dans les tests aux classes et méthodes ayant une visibilité package.
C'est possible en utilisant les options classpath et destination de javac et java.
Voir exemples/java/junit5/maven-dirs, qui reproduit la hiérarchie par défaut de maven :
maven-dirs
    ├── bin
    │   └── project1
    │       ├── Project1.class
    │       └── Project1Test.class
    └── src
         ├── main
         │   └── java
         │       └── project1
         │           └── Project1.java
         └── test
             └── java
                 └── project1
                     └── Project1Test.java
On compile d'abord le code à tester dans bin
Puis on compile le code de test en incluant bin dans le classpath :
javac -d bin src/main/java/project1/Project1.java
javac -cp /path/to/junit-platform-console-standalone-1.3.1.jar:bin -d bin src/test/java/project1/Project1Test.java