Module : Qualité & Tests
Les tests unitaires en Java
Exercices Pratiques
Prérequis :
• Comment installer et utiliser Eclipse pour programmer et tester en Java ?
• Quels sont les concepts de base et les types des tests unitaires en Java ?
• Pourquoi développer des tests unitaires en Java avec les annotations JUnit ?
• Comment tester unitairement un code Java pour réaliser une application de qualité ?
Objectif principal :
Dans ce tutoriel avec des exercices pratiques, notre objectif est de bien structurer les tests
unitaires pour réaliser une application de qualité en Java avec les annotations JUnit.
Durée : deux séances.
Informations nécessaires :
• Les tests unitaires : consistent à isoler une partie du code et à vérifier qu'il fonctionne
parfaitement. Il s'agit de petits tests qui valident l'attitude d'un objet et la logique du code.
Les tests unitaires sont généralement effectués pendant la phase de développement des
applications mobiles ou logicielles [1]. Pour tester unitairement un code Java, il faut
remplir les méthodes de test suivantes :
➢ Instancier la classe à tester T ;
➢ Initialiser T ;
➢ Générer les arguments pour la méthode à tester ;
➢ Générer le résultat ;
➢ Tester la méthode avec les arguments ;
➢ Vérifier le résultat ;
➢ Recommencer depuis 3 tant qu'il y a des cas à tester.
• La framework JUnit :
JUnit est un framework open source pour le développement et l'exécution de tests
unitaires automatisables. Le principal intérêt est de s'assurer que le code répond
toujours aux besoins même après d'éventuelles modifications. Plus généralement,
ce type de tests est appelé tests unitaires de non-régression [2]. JUnit est sûrement
le logiciel ou le framework de test le plus utilisé. Ce logiciel peut être téléchargé à
l'adresse suivante : [Link]
• L’annotation Java :
C'est un mot clé précédé du symbole “arobase”, que l'on place juste au-dessus d'un
élément Java en donnant une information précise pour décrire cet élément [3]. Par
exemples : un nom de classe, de méthode, ou même de paramètre de méthode.
Elles peuvent vous aider à rendre vos tests plus pertinents et vous économiser des
lignes de code. Pour utiliser ces annotations, il suffit de les ajouter juste avant la ligne
qui déclare votre classe de test ou votre méthode.
• Dans le cas des tests, le framework JUnit 5 va se servir des annotations pour savoir
comment lancer les tests. Il offre de nombreuses annotations utiles, et je vais vous
en présenter quelques-unes dans les exercices pratiques suivants. Les annotations
JUnit vous aident à écrire des tests plus clairs sans répétitions inutiles. Découvrir les
annotations principales à partir le framework JUnit 5.
Voici quelques annotations courantes :
Annotation Quand l’utiliser
Exécutez une méthode avant chaque test. C’est un très bon
@BeforeEach emplacement pour installer ou organiser un prérequis pour
vos tests.
Exécutez une méthode après chaque test. C’est un très bon
@AfterEach emplacement pour nettoyer ou satisfaire à une
postcondition.
@BeforeAll Désignez une méthode statique pour qu’elle soit exécutée
avant tous vos tests. Vous pouvez l’utiliser pour installer
d’autres variables statiques pour vos tests.
@AfterAll Désignez une méthode statique pour qu’elle soit exécutée
après tous vos tests. Vous pouvez utiliser ceci pour nettoyer
les dépendances statiques.
Création d’un projet Eclipse avec JUnit 5.x
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
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 [Link] 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.
Ceux qui ont étudié la mise en œuvre 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 [Link], 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.
Un premier test JUnit pour vérifier l'addition de deux nombres rationnels
package [Link];
import static [Link].*;
import [Link];
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 = [Link]( r2 );
// On vérifie les résultats
assertEquals( 7 /* Valeur attendue */, [Link]() /* Valeur constatée */);
assertEquals( 3 /* Valeur attendue */, [Link]() /* Valeur constatée */);
}
}
Les méthodes commençant par assert sont portées par classe [Link] sous
forme de membres statiques. Mais comme votre classe de test porte un import static
[Link].* (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 [Link] et vous pourrez invoquer, par exemple, la
méthode [Link](...).
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.
package [Link];
import static [Link].*;
import [Link];
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 = [Link]( r2 );
// On vérifie les résultats
assertEquals( 7 /* Valeur attendue */, [Link]() /* Valeur constatée */);
assertEquals( 3 /* Valeur attendue */, [Link]() /* 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 );
[Link]();
// On vérifie les résultats
assertEquals( 35, [Link]() );
assertEquals( 17, [Link]() );
}
}
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.
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.
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.
En cliquant avec le bouton droit de la souris sur le nom d'une méthode : vous lancerez que cette
méthode de test. Si dans l'immédiat, vous ne souhaitez lancer que ce test et que la procédure de
test nécessite un certain temps pour son exécution, vous limiterez le temps passé dans JUnit. Voici
une capture d'écran montrant cette possibilité.
• En cliquant avec le bouton droit de la souris sur le nom d'une classe de test, toutes les méthodes
de tests de cette classe seront lancées.
• En cliquant dans l'explorateur de projets sur le nom d'un fichier : cela revient au même que de
cliquer sur le nom de la classe de test contenu dans ce fichier. Voici une capture d'écran montrant
cette possibilité.
En cliquant dans l'explorateur de projets sur le nom d'un package : dans ce cas, toutes les classes
de tests présentes dans le package seront exécutées. On peut alors vraiment commencer à parler
de jeu (ou de batterie) de tests. Voici une capture d'écran montrant cette possibilité.
En cliquant dans l'explorateur de projets sur un source folder : toutes les classes de tests présentes
dans tous les packages de ce dossier de code source seront exécutées. Voici une capture d'écran
montrant cette possibilité.
En cliquant dans l'explorateur de projets sur le projet : toutes les classes de tests de tous les dossiers
de codes sources seront alors exécutées. Voici une capture d'écran montrant cette possibilité.
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 [Link] qui sert à valider la remontée d'une exception.
Cette méthode requière une instance de type [Link] : 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.
package [Link];
import static [Link];
import static [Link];
import [Link];
class RationalTest {
// Les autres méthodes de tests
// ...
// On teste un déclenchement d'exception.
@Test
public void testBadDenominator() {
assertThrows( [Link], () -> {
new Rational( 1, 0 );
});
}
}
Et voici les résultats produits par notre nouveau jeu de tests.
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
à [Link] permet de réaliser ce dépassement.
package [Link];
import [Link];
import [Link];
import [Link];
class TimeoutTest {
@Test
@Timeout( value=10, unit = [Link] )
void test() throws Exception {
[Link]( 1000 /* Milliseconds */ );
}
}
Et voici comme le problème vous est restitué par JUnit dans l'intégration Eclipse.
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 fallu à 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
On se base 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.
package [Link];
import [Link];
import [Link];
import [Link];
import [Link];
import [Link];
class LifeCycleTest {
@BeforeAll
static void setUpBeforeClass() throws Exception {
[Link]( "@BeforeAll" );
}
@Test
public void test1() {
[Link]( "test1" );
}
@Test
public void test2() {
[Link]( "test2" );
}
@Test
public void test3() {
[Link]( "test3" );
}
@AfterAll
static void tearDownAfterClass() throws Exception {
[Link]( "@AfterAll" );
}
}
Voici les résultats produits par cette classe de test (regarder dans la vue « Console »).
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.
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.
On a 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.
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.
package [Link];
import [Link];
import [Link];
import [Link];
import [Link];
import [Link];
class LifeCycleTest {
@BeforeAll
static void setUpBeforeClass() throws Exception {
[Link]( "@BeforeAll" );
}
@BeforeEach
void setUp() throws Exception {
[Link]( "@BeforeEach" );
}
@Test
public void test1() {
[Link]( "test1" );
}
@Test
public void test2() {
[Link]( "test2" );
}
@Test
public void test3() {
[Link]( "test3" );
}
@AfterEach
void tearDown() throws Exception {
[Link]( "@AfterEach" );
}
@AfterAll
static void tearDownAfterClass() throws Exception {
[Link]( "@AfterAll" );
}
}
Et voici les résultats qui seront affichés dans la console Eclipse suite à l'exécution de vos
tests.
Bonnes pratiques de développement JUnit
• Une méthode de test doit vérifier un point bien précis.
• Chaque méthode de test doit être la plus rapide possible, afin de garantir l'exécution de
la procédure de tests unitaires le plus fréquemment possible.
• Vous ne pas présager de l'ordre d'exécution des méthodes de test. Cela sera fait au bon
vouloir de JUnit.
• Si vos méthodes de tests utilisent des attributs de la classe de test (dit autrement un
contexte), vous devez réinitialiser ce contexte avant chaque exécution d'une méthode de
test (@BeforeEach) et le nettoyer après aussi après chaque méthode (@AfterEach).