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
Cette vidéo vous montre comment mettre en oeuvre une batterie de tests unitaires avec le framework JUnit 5.
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.
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.
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).
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
Voici à quoi doit ressembler votre projet à ce stade.
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.
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.
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.
Arrivé à ce stade, voici à quoi doit ressembler votre projet.
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
.
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.
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 */); } } |
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é.
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() ); } } |
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.
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).
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.
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 ); }); } } |
Et voici les résultats produits par notre nouveau jeu de tests.
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.
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 */ ); } } |
Et voici comme le problème vous est restitué par JUnit dans l'intégration Eclipse.
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
.
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" ); } } |
Voici les résultats produits par cette classe de test (regarder dans la vue « Console »).
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
.
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.
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" ); } } |
Et voici les résultats qui seront affichés dans la console Eclipse suite à l'exécution de vos tests.
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 :