Accès rapide :
Création d'un projet Eclipse avec le support JUnit 4.x
L'utilisation des annotations avec JUnit 4
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 4
Les annotations @BeforeClass et @AfterClass
Les annotations @Before et @After
Bonnes pratiques de développement JUnit
La version 4 de JUnit a changé la manière de décrire nos tests. Alors que les différentes versions 3.x vous demandaient de respecter des conventions de codage, JUnit 4.0 a préféré privilégier l'utilisation d'annotations. Il y a donc eu une rupture de compatibilité : sur le fond on continue donc à faire à peu près la même chose, mais c'est la forme (la syntaxe de mise en oeuvre) qui a donc changé.
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 4 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.Assert
, 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 la classe TestCase
que vous aviez l'habitude d'utiliser.
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
, 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 4
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.Assert.*; import org.junit.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.Assert
sous forme de membres statiques.
Mais comme votre classe de test porte un import static org.junit.Assert.*
(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.Assert
et vous
pourrez invoquer, par exemple, la méthode Assert.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.Assert.*; import org.junit.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 */); } // 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 4 permet, par le biais de l'annotation Test
et de sa propriété expected
,
de spécifier qu'une exception est attendue. Du coup, si votre code ne déclenche pas l'exception attendue, le test sera considéré comme échoué.
Voici un exemple d'utilisation de cette possibilité.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
package fr.koor.poo; import static org.junit.Assert.*; import org.junit.Test; public class RationalTest { // Les autres méthodes de tests // ... // On teste un déclenchement d'exception. @Test( expected = RuntimeException.class ) public void testBadDenominator() { new Rational( 1, 0 ); } } |
Et voici les résultats produits par notre nouveau jeu de tests.
JUnit 4 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 la propriété timeout
à l'annotation @Test
. Le délai est exprimé en millisecondes.
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 |
package fr.koor.poo; import org.junit.Test; public class TimeoutTest { @Test( timeout = 10 /* 10 millisecondes */ ) public void test() throws InterruptedException { Thread.sleep( 1000 /* 1 secondes */ ); } } |
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 4.
Outre vos méthodes de tests vous pouvez ajouter jusqu'à quatre méthodes complémentaires, grâce à quatre annotations supplémentaires :
@BeforeClass
, @AfterClass
, @Before
et @After
.
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 être publiques, 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 |
package fr.koor.poo; import org.junit.AfterClass; import org.junit.BeforeClass; import org.junit.Test; public class LifeCycleTest { @BeforeClass public static void setUpBeforeClass() throws Exception { System.out.println( "@BeforeClass" ); } @Test public void test1() { System.out.println( "test1" ); } @Test public void test2() { System.out.println( "test2" ); } @Test public void test3() { System.out.println( "test3" ); } @AfterClass public static void tearDownAfterClass() throws Exception { System.out.println( "@AfterClass" ); } } |
Voici les résultats produits par cette classe de test (regarder dans la vue « Console »).
La méthode annotée @Before
, 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 @Before
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 @After
. 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 @After
.
setUp
(pour celle annotée avec @Before
) 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 être publiques, 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.After; import org.junit.AfterClass; import org.junit.Before; import org.junit.BeforeClass; import org.junit.Test; public class LifeCycleTest { @BeforeClass public static void setUpBeforeClass() throws Exception { System.out.println( "@BeforeClass" ); } @Before public void setUp() { System.out.println( "@Before" ); } @Test public void test1() { System.out.println( "test1" ); } @Test public void test2() { System.out.println( "test2" ); } @Test public void test3() { System.out.println( "test3" ); } @After public void tearDown() { System.out.println( "@After" ); } @AfterClass public static void tearDownAfterClass() throws Exception { System.out.println( "@AfterClass" ); } } |
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 :