Participer au site avec un Tip
Rechercher
 

Améliorations / Corrections

Vous avez des améliorations (ou des corrections) à proposer pour ce document : je vous remerçie par avance de m'en faire part, cela m'aide à améliorer le site.

Emplacement :

Description des améliorations :

Mise en oeuvre de tests unitaires avec JUnit 5.x

JUnit 4.x Code Coverage



Accès rapide :
La vidéo
Introduction à JUnit 5 (Jupiter)
Création d'un projet Eclipse avec le support JUnit 5.x
L'utilisation des annotations avec JUnit 5
Lancer vos tests JUnit
Tester un déclenchement d'exceptions
Imposer un temps maximal pour l'exécution d'un test
Le cycle de vie d'une classe de test JUnit 5
Les annotations @BeforeAll et @AfterAll
Les annotations @BeforeEach et @AfterEach
Bonnes pratiques de développement JUnit

La vidéo

Cette vidéo vous montre comment mettre en oeuvre une batterie de tests unitaires avec le framework JUnit 5.


Mise en oeuvre de tests unitaires avec JUnit 5

Introduction à JUnit 5 (Jupiter)

La version 5 de JUnit constitue une nouvelle refonte de l'outil. Il y a encore une fois rupture de compatibilité avec les versions antérieures. Pour rappel, l'API JUnit 3 était placée dans le package junit.framework alors que l'API de la version 4 était placée dans le package org.junit. Savoir si vous utilisez une version 5 de l'API JUnit sera simple : ses classes, interfaces et annotations sont maintenant situé dans le package org.junit.jupiter et ses sous-packages.

Jupiter est le nom de code de la version 5 de JUnit.

A l'instar de JUnit 4, cette nouvelle version est basée, en grande partie, sur l'utilisation d'un jeu d'annotations. Mais cette nouvelle version exploite les nouveautés de Java 8 et notamment les lambdas et les annotations répétées. Elle propose aussi des mécanismes d'extension du framework de test.

Création d'un projet Eclipse avec le support JUnit 5.x

Dans la suite de ce document, nous allons chercher à tester le code développé durant le chapitre sur l'introduction à la programmation orientée objet. Ce code proposait une simple classe de manipulation de nombre rationnels (de fractions, si vous préférez).

oui je sais, ce n'est pas un vrai composant logiciel comme suggéré dans le chapitre d'introduction aux tests unitaires, mais je préfère débuter par un exemple relativement simple.

Nous allons tester ce code en exploitant l'intégration de JUnit au sein d'Eclipse. Donc commencez par créer un nouveau projet : vous pouvez l'appeler Rational. Copiez/collez-y le code de la classe Rational à partir du lien ci-dessus.

Il faut comprendre que mélanger les codes applicatifs et les codes de tests, n'est pas forcément une bonne chose. Eclipse propose une fonctionnalité qui va nous aider à éviter cela : la notion de « Source Folder ». Un tel dossier contient, comme son nom l'indique, des codes sources et force leur compilation. Clairement, le répertoire src est un dossier de cette nature. Pour créer un dossier de codes sources dans votre projet, cliquez avec le bouton droit de la souris sur le projet puis sélectionnez « New / Source Folder ». Appelez ce dossier test

si vous sélectionnez « New / Folder » à la place de « New / Source Folder », vous aurez créé un simple dossier. Oui vous pourrez y mettre des fichiers Java, mais ils ne seront pas compilés.

Voici à quoi doit ressembler votre projet à ce stade.

Création d'un source folder pour nos tests.

Il faut maintenant ajouter une classe de test. Pour ce faire, cliquez avec le bouton droit de la souris sur le dossier test et sélectionnez-y l'assistant « New / JUnit Test Case », comme le montre la capture d'écran suivante.

Ajout d'une classe de test.

Une nouvelle boîte de dialogue doit apparaître. Notez bien qu'en haut de la fenêtre, vous pouvez choisir la version de JUnit à utiliser. Veuillez cocher la case « New JUnit Jupiter test ». Veuillez ensuite remplir le champ « Package » avec la valeur fr.koor.poo ainsi que le champ « Name » avec la valeur RationalTest. Voici une capture d'écran de cette boîte de dialogue.

Ajout d'une classe de test et choix de la version de JUnit.

Comme c'est la première fois qu'on ajoute une classe de test à ce projet, la librairie JUnit n'y est pas encore présente. Eclipse détecte cette situation et vous propose d'ajouter automatiquement la librairie au ClassPath. Accepter, bien entendu, cette proposition.

Ajout de JUnit dans le classpath.

Arrivé à ce stade, voici à quoi doit ressembler votre projet.

Ajout d'une classe de test au projet Eclipse.
ceux qui ont étudié la mise en oeuvre de tests unitaires avec JUnit 3 auront noté un premier changement majeur : nous ne sommes plus obligés de dériver de la classe TestCase. Cela vous laisse donc toute liberté en termes d'héritage. Du coup, nous n'avons plus accès aux méthodes de cette classe. C'est pour cela que la méthode fail a été déplacée sur la classe org.junit.jupiter.api.Assertions, sous forme d'un membre statique (ce qui explique l'import statique en ligne 3). Il en va de même pour les autres méthodes de l'ancienne classe TestCase que vous aviez l'habitude d'utiliser. Bien entendu, vous aurez aussi remarqué la présence de l'annotation @Test.

L'utilisation des annotations avec JUnit 5

Aucune règle particulière n'est requise pour définir une classe de test. Personnellement, j'ai quand même l'habitude de la suffixer du terme Test. Il sera ainsi facile de reconnaître une classe de test d'une autre. Je vous ai donc demandé de créer la classe RationalTest.

Pour définir une méthode de test, il faut lui adjoindre l'annotation @Test (celle du package org.junit.jupiter.api.), mais ce n'est pas suffisant. Il faut impérativement qu'une telle méthode soit publique, qu'elle n'ait pas de valeur de retour (void) et qu'elle n'accepte aucun paramètre. Sans quoi, les méthodes ne respectant pas ces règles ne seront pas prises en compte par JUnit.

personnellement, j'ai tendance à continuer à aussi respecter une autre règle de JUnit 3 (les habitudes sont tenaces). Effectivement, je préfixe mes méthodes de test par les quatre lettres test. Mais j'insiste, ce n'est plus une obligation avec JUnit 5 et cela ne vous affranchit absolument pas de devoir mettre l'annotation @Test.

Un petit conseil : privilégiez des tests simples. Si vous avez deux choses à vérifier, définissez deux méthodes de tests chacune ne vérifiant que l'un des deux aspects.

Voici un premier exemple de test qui va vérifier si l'addition de deux nombres rationnels se passe bien.

 1 
 2 
 3 
 4 
 5 
 6 
 7 
 8 
 9 
 10 
 11 
 12 
 13 
 14 
 15 
 16 
 17 
 18 
 19 
 20 
 21 
 22 
 23 
package fr.koor.poo;

import static org.junit.jupiter.api.Assertions.*;

import org.junit.jupiter.api.Test;

public class RationalTest {

    // Une méthode de test doit être annotée par @Test.
    @Test
    public void testAddition() {
        
        // On prépare le scénario de test
        Rational r1 = new Rational( 1, 3);
        Rational r2 = new Rational( 2, 1);
        Rational result = r1.add( r2 );
        
        // On vérifie les résultats
        assertEquals( 7 /* Valeur attendue */, result.getNumerator() /* Valeur constatée */);
        assertEquals( 3 /* Valeur attendue */, result.getDenominator() /* Valeur constatée */);
        
    }
}
Un premier test JUnit pour vérifier l'addition de deux nombres rationnels

Les méthodes commençant par assert sont portées par la classe org.junit.jupiter.api.Assertions sous forme de membres statiques. Mais comme votre classe de test porte un import static org.junit.jupiter.api.Assertions.* (ligne 3) aucun préfixe n'est requis devant les appels à ces méthodes. Elles permettent de vérifier vos résultats. Si l'assertion est vraie, le test se poursuit normalement est aucun message d'erreur sera produit. Dans le cas contraire, ces méthodes déclenchent des exceptions permettant d'interrompre le test et de le passer dans l'état échoué.

si vous n'êtes pas fan des import static, vous pouvez simplement importer la classe org.junit.jupiter.api.Assertions et vous pourrez invoquer, par exemple, la méthode Assertions.assertEquals(...).

Vous pouvez y ajouter un second test pour vérifier si la simplification de fraction se passe correctement. Voici un exemple de code pour ce second test.

 1 
 2 
 3 
 4 
 5 
 6 
 7 
 8 
 9 
 10 
 11 
 12 
 13 
 14 
 15 
 16 
 17 
 18 
 19 
 20 
 21 
 22 
 23 
 24 
 25 
 26 
 27 
 28 
 29 
 30 
 31 
 32 
 33 
 34 
 35 
 36 
 37 
 38 
package fr.koor.poo;

import static org.junit.jupiter.api.Assertions.*;

import org.junit.jupiter.api.Test;

class RationalTest {

    // Une méthode de test doit être annotée par @Test.
    @Test
    public void testAddition() {
        
        // On prépare le scénario de test
        Rational r1 = new Rational( 1, 3);
        Rational r2 = new Rational( 2, 1);
        Rational result = r1.add( r2 );
        
        // On vérifie les résultats
        assertEquals( 7 /* Valeur attendue */, result.getNumerator() /* Valeur constatée */);
        assertEquals( 3 /* Valeur attendue */, result.getDenominator() /* Valeur constatée */);
        
    }

    // La seconde méthode de test
    @Test
    public void testSimplify() {
    
        // On prépare le scénario de test
        Rational r = new Rational( 5*7*11*13, 11*13*17 );
        r.simplify();

        // On vérifie les résultats
        assertEquals( 35, r.getNumerator() );
        assertEquals( 17, r.getDenominator() );
    
    }
    
}
Un second test JUnit pour vérifier la simplification de fraction

Lancer vos tests JUnit

Pour lancer le jeu de tests, veuillez cliquer avec le bouton droit de la souris sur le nom de la classe et lancez l'assistant « Run As / JUnit Test ». Les résultats des tests sont affichés dans la vue « JUnit » comme le montre la capture d'écran suivante.

Lancement de votre jeu de tests dans JUnit.

La vue « JUnit » affiche donc les résultats. Pour chaque test lancé, vous avez l'information sur le temps qu'il a pris durant son exécution. On y voit aussi que nous avons démarré deux tests et que nous avons tous ces tests en succès et aucun en échec. En conséquence, on y voit la barre de statut complétement verte. Dans le cas contraire elle aurait été rouge. Voici un exemple de détection d'un test en échec (j'ai modifié le code de la méthode add pour que le calcul se passe mal).

Détection d'un problème par JUnit.

En fait, les possibilités de démarrage de vos tests sont plus subtiles qu'il n'y parait. Vous pouvez choisir « la quantité » de tests à exécuter en cliquant à différents endroits de l'interface graphique d'Eclipse.

Notez aussi que, si vous avez déjà lancé un ensemble de tests et que vous souhaitez relancer cet ensemble, vous pouvez cliquer sur le bouton « Rerun Test », comme le montre la capture d'écran ci-dessous.

Relancer un jeu de tests.

Tester un déclenchement d'exceptions

Tester une application ne veut pas dire tester que ce qui doit marcher. Il faut aussi vérifier que tous les cas d'erreur connus sont bien détectés. Dans notre cas, il n'est normalement pas possible de créer une fraction avec la valeur 0 en dénominateur. Il faut donc tester qu'on détecte bien ce type de problème.

Le souci est que si une exception remonte à JUnit, le scénario de test sera considéré comme étant échoué. Comment inverser les choses ? En fait, c'est assez simple : JUnit 5 propose la méthode statique Assertions.assertThrows qui sert à valider la remontée d'une exception. Cette méthode requière une instance de type org.junit.jupiter.api.function.Executable : il s'agit d'une interface fonctionnelle exposant la méthode execute qui doit contenir le code à exécuter. Comme il s'agit d'une interface fonctionnelle, vous pouvez l'implémenter soit via une classe anonyme, soit via une expression lambda ou encore une référence sur méthode.

Si votre code testé ne déclenche pas l'exception attendue, le test sera considéré comme échoué. Voici un exemple d'utilisation de cette possibilité : l'interface fonctionnelle y est réalisée via une expression lambda.

 1 
 2 
 3 
 4 
 5 
 6 
 7 
 8 
 9 
 10 
 11 
 12 
 13 
 14 
 15 
 16 
 17 
 18 
 19 
 20 
 21 
package fr.koor.poo;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;

import org.junit.jupiter.api.Test;

class RationalTest {

    // Les autres méthodes de tests
    // ...
    
    // On teste un déclenchement d'exception.
    @Test
    public void testBadDenominator() {
        assertThrows( RuntimeException.class, () -> {
            new Rational( 1, 0 );
        });
    }

}
Tester le déclenchement d'une exception

Et voici les résultats produits par notre nouveau jeu de tests.

On test un déclechement d'exception.
pour ceux qui connaissent déjà JUnit 4 et l'utilisation de la propriété expected sur l'annotation @Test, l'approche JUnit 5 est plus permissive. Effectivement vous pouvez tester plusieurs remontées d'exceptions successives via la nouvelle approche.

Imposer un temps maximal pour l'exécution d'un test

JUnit 5 permet d'imposer un temps maximal d'exécution pour un test. Si ce dernier dépasse le temps imparti, il sera stoppé et considéré comme étant échoué. Pour fixer ce temps maximal, il faut ajouter l'annotation @Timeout. Vous pouvez contrôler la durée et l'unité de temps dans laquelle elle est exprimée. Voici un exemple de test dépassant le temps maximal imposé : un simple appel à Thread.sleep permet de réaliser ce dépassement.

 1 
 2 
 3 
 4 
 5 
 6 
 7 
 8 
 9 
 10 
 11 
 12 
 13 
 14 
 15 
 16 
package fr.koor.poo;

import java.util.concurrent.TimeUnit;

import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.Timeout;

class TimeoutTest {

    @Test 
    @Timeout( value=10, unit = TimeUnit.MILLISECONDS )
    void test() throws Exception {
        Thread.sleep( 1000 /* Milliseconds */ );
    }

}
Exemple de dépassement du délai maximal imposé pour un test

Et voici comme le problème vous est restitué par JUnit dans l'intégration Eclipse.

Exemple d'utilisation d'un timeout sur un test JUnit.
dans la capture d'écran, le test passé par le test est légèrement supérieur à 10ms (ici 0,034s). C'est le temps qu'il a fallut à JUnit de détecter le dépassement et interrompre le thread associé à votre test JUnit. Mais au final, il aura duré bien moins qu'une seconde.

Le cycle de vie d'une classe de test JUnit 5

Pour clore ce chapitre, je voudrais porter mon attention sur les méthodes relatives au cycle de vie d'une classe de test JUnit 5. Outre vos méthodes de tests vous pouvez ajouter jusqu'à quatre méthodes complémentaires, grâce à quatre annotations supplémentaires : @BeforeAll, @AfterAll, @BeforeEach et @AfterEach.

Les annotations @BeforeAll et @AfterAll

Ces deux annotations permettent de marquer deux méthodes s'exécutant une unique fois au lancement et à l'arrêt de la classe de tests. Les méthodes associées doivent statiques, ne pas renvoyer de valeur (void) et n'accepter aucun paramètre. Par contre, vous avez toute latitude sur le nom des méthodes : l'IDE Eclipse les nomme setUpBeforeClass et tearDownAfterClass.

Elles peuvent vous permettre d'initialiser et de libérer un contexte utilisé par vos méthodes de tests mais ne nécessitant pas d'être réinitialisé entre chaque méthode de test. Voici un exemple d'utilisation de ces deux annotations.

 1 
 2 
 3 
 4 
 5 
 6 
 7 
 8 
 9 
 10 
 11 
 12 
 13 
 14 
 15 
 16 
 17 
 18 
 19 
 20 
 21 
 22 
 23 
 24 
 25 
 26 
 27 
 28 
 29 
 30 
 31 
 32 
 33 
 34 
 35 
 36 
package fr.koor.poo;

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.Test;

class LifeCycleTest {

    @BeforeAll
    static void setUpBeforeClass() throws Exception {
        System.out.println( "@BeforeAll" );
    }

    @Test
    public void test1() {
        System.out.println( "test1" );
    }

    @Test
    public void test2() {
        System.out.println( "test2" );
    }

    @Test
    public void test3() {
        System.out.println( "test3" );
    }

    @AfterAll
    static void tearDownAfterClass() throws Exception {
        System.out.println( "@AfterAll" );
    }

}
Exemple d'utilisation des annotations @BeforeAll et @AfterAll

Voici les résultats produits par cette classe de test (regarder dans la vue « Console »).

Exemple d'utilisation des annotations @BeforeAll et @AfterAll
il est possible de générer les déclarations de ces méthodes grâce à l'assistant JUnit d'Eclipse. La capture d'écran suivante vous montre les deux cases à cocher à activer pour obtenir ces méthodes.
Génération de méthodes annotatées via @BeforeAll et @AfterAll

Les annotations @BeforeEach et @AfterEach

La méthode annotée @BeforeEach, si elle est définie, est invoquée avant l'exécution de chaque méthode de test. Dans notre classe de test, nous avons trois méthodes annotées par @Test, la méthode marquée @BeforeEach sera donc lancée trois fois. Par exemple, si chacun de vos tests nécessite une connexion à une base de données, il ne faut pas que l'exécution du test précédent mette en péril l'exécution de la méthode de test courante, par exemple avec des transactions en cours ou encore une fermeture de connexion à la base. Le mieux est donc de réouvrir une connexion propre avant chaque exécution de test.

De manière symétrique, il existe une possibilité pour définir une méthode de libération d'un éventuel contexte : il faudra annoter la méthode avec @AfterEach. Elle sera exécutée à la fin de chaque méthode de test et dans notre cas, trois fois. Si l'on reprend l'exemple de l'utilisation d'une connexion à une base de données pour chacun de vos tests, dans ce cas, vous pouvez fermer chaque connexion à la fin du test via la méthode marquée avec l'annotation @AfterEach.

j'ai l'habitude d'appeler ces méthodes setUp (pour celle annotée avec @BeforeEach) et tearDown (pour l'autre). Ces deux noms viennent en fait des anciennes conventions de nommage de JUnit3. Les habitudes étant tenaces, j'ai du mal à m'en défaire. Mais vous pouvez opter pour n'importe quel autre nom.

Outre le fait d'annoter correctement ces deux méthodes, elles doivent aussi ne rien renvoyer et ne pas accepter de paramètre. Si vous ne respectez pas ces règles, des erreurs seront produites.

Lors de la création d'une nouvelle classe de test, vous pouvez demander à Eclipse de produire ces méthodes en cochant les cases associées, comme le montre la capture d'écran suivante.

Exemple de génération des méthodes setUp et tearDown par Eclipse.
oui, Eclipse continue, lui aussi, à utiliser les anciennes conventions de noms (setUp et tearDown).

Voici un exemple basique de définition de ces deux méthodes : l'objectif est simplement de vous montrer qu'elles déclenchent avant est après chaque appel à une méthode de test. On y retrouve aussi les deux précédentes méthodes, annotées @BeforeClass et @AfterClass.

 1 
 2 
 3 
 4 
 5 
 6 
 7 
 8 
 9 
 10 
 11 
 12 
 13 
 14 
 15 
 16 
 17 
 18 
 19 
 20 
 21 
 22 
 23 
 24 
 25 
 26 
 27 
 28 
 29 
 30 
 31 
 32 
 33 
 34 
 35 
 36 
 37 
 38 
 39 
 40 
 41 
 42 
 43 
 44 
 45 
 46 
package fr.koor.poo;

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.Test;

class LifeCycleTest {

    @BeforeAll
    static void setUpBeforeClass() throws Exception {
        System.out.println( "@BeforeAll" );
    }

    @BeforeEach
    void setUp() throws Exception {
        System.out.println( "@BeforeEach" );
    }

    @Test
    public void test1() {
        System.out.println( "test1" );
    }

    @Test
    public void test2() {
        System.out.println( "test2" );
    }

    @Test
    public void test3() {
        System.out.println( "test3" );
    }

    @AfterEach
    void tearDown() throws Exception {
        System.out.println( "@AfterEach" );
    }

    @AfterAll
    static void tearDownAfterClass() throws Exception {
        System.out.println( "@AfterAll" );
    }

}
Exemple d'utilisation des annotations @BeforeEach et @AfterEach

Et voici les résultats qui seront affichés dans la console Eclipse suite à l'exécution de vos tests.

Exemple d'exécution des méthodes annotées @BeforeEach et @AfterEach.

Bonnes pratiques de développement JUnit



JUnit 4.x Code Coverage