Tests unitaires avec JUnit 5

Prise en main

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

Les tests sont organisés en utilsant des annotations (voir la liste complète).
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);
    }

}
(code dans 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.8.2).
Télécharger junit-platform-console-standalone-1.8.2.jar et le stocker en local.
(une copie se trouve sur le répertoire 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.8.2.jar -h
# ou plus simplement
java -jar /path/to/junit-platform-console-standalone-1.8.2.jar

Compiler / exécuter

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

Avec Eclipse

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 annnotées avec @Test ; elles vont être exécutées par JUnit et leur résultat va être inclus dans le rapport.

Assertions

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.

Autres annotations courantes : ATTENTION : lorsque vous utilisez assertEquals() avec des float ou des double, utilisez les méthodes avec un paramètre delta, par exemple :
assertEquals​(float expected, float actual, float delta)
qui signifie "assert presque égal" (égalité stricte impossible due à la représentation binaire des nombres réels).

Exemple

Liste de String contenant ces méthodes publiques :
public class CustomList{

    /** Renvoie le nombre d'élément dans la liste **/
    public int getLength(){ ... }
    
    /** Ajoute un élément à la fin de la liste **/
    public void add(String s){ ... }
    
    /** Supprime un élément de la liste **/
    public void 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;
    
    @BeforeAll
    static void setup() {
        // exécutée une fois avant tous les tests
    }
    
    @BeforeEach
    void init() {
        l1 = new CustomList();
    }
    
    @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
    }
    
    //
    // Méthodes de tests unitaires
    //
    
    @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);
        }
    }

Test Suite : tester plusieurs classes

En plus de junit-platform-console-standalone-1.8.2.jar, les jars suivants doivent être dans le classpath (à la fois pour compiler et pour exécuter) : (une copie de ces jars se trouve sur le répertoire bin/ de ce cours)

Exemple : suite de tests pour le TP2 : conversion
package conversion;

import conversion.model.DegresTest;
import conversion.utils.FormatTest;

import org.junit.platform.suite.api.Suite;
import org.junit.platform.suite.api.SuiteDisplayName;
import org.junit.platform.suite.api.SelectClasses;

@Suite
@SuiteDisplayName("TP2 conversion test suite")
@SelectClasses({DegresTest.class, FormatTest.class})
public class ConversionTestSuite {}
On signale à JUnit une suite de tests en utilisant @Suite.
Dans cet exemple, on a utilisé @SelectClasses pour indiquer quelles classes de tests unitaires exécuter.
D'autres annotations permettent de spécifier les classes de test (@SelectPackages etc.), voir par exemple JUnit5 User Guide ou https://howtodoinjava.com/junit5/junit5-test-suites-examples

Compilation

test-suite-compile
#!/bin/sh

dir_bin='../../bin/tps/2-conversion'
dir_jar='../../../bin'

jar1_junit="$dir_jar/junit-platform-console-standalone-1.8.2.jar"
jar2_junit="$dir_jar/junit-platform-suite-api-1.8.2.jar"
jar3_junit="$dir_jar/junit-platform-suite-commons-1.8.2.jar"
jar4_junit="$dir_jar/junit-platform-suite-engine-1.8.2.jar"

# compile chacun des tests
./test-compile

# compile la suite de tests
command="javac -d $dir_bin -cp $jar1_junit:$jar2_junit:$jar3_junit:$jar4_junit:$dir_bin tests/conversion/ConversionTestSuite.java"
echo $command
$command
Avant de compiler la suite, chaque test doit être compilé :
test-compile
#!/bin/sh

# Cette commande peut être exécutée directement
# ou être exécutée depuis test-suite-compile

dir_bin='bin'
jar_junit='../../../bin/junit-platform-console-standalone-1.8.2.jar'

command="javac -d $dir_bin -cp $jar_junit:$dir_bin tests/conversion/utils/FormatTest.java"
echo $command
$command

command="javac -d $dir_bin -cp $jar_junit:$dir_bin tests/conversion/model/DegresTest.java"
echo $command
$command

Exécution

test-suite-run
#!/bin/sh

dir_bin='../../bin/tps/2-conversion'
dir_jar='../../../bin'

jar1_junit="$dir_jar/junit-platform-console-standalone-1.8.2.jar"
jar2_junit="$dir_jar/junit-platform-suite-api-1.8.2.jar"
jar3_junit="$dir_jar/junit-platform-suite-commons-1.8.2.jar"
jar4_junit="$dir_jar/junit-platform-suite-engine-1.8.2.jar"

command="java -jar $jar1_junit -cp $dir_bin:$jar2_junit:$jar3_junit:$jar4_junit -c conversion.ConversionTestSuite"
echo $command
$command

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 (-cp) et destination (-d) 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.8.2.jar:bin -d bin src/test/java/project1/Project1Test.java