Développons en Java v 2.40 Copyright (C) 1999-2023 Jean-Michel DOUDOUX. |
|||||||
Niveau : | Supérieur |
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. |
JUnit a été initialement développé par Erich Gamma et Kent Beck.
JUnit propose :
Le but est d'automatiser les tests. Ceux-ci sont exprimés dans des classes sous la forme de cas de tests avec leurs résultats attendus. JUnit exécute ces tests et les comparent avec ces résultats.
Cela permet de séparer le code de la classe, du code qui permet de la tester. Souvent pour tester une classe, il est facile de créer une méthode main() qui va contenir les traitements de tests. L'inconvénient est que ce code "superflu" est inclus dans la classe. De plus, son exécution doit se faire manuellement.
La rédaction de cas de tests peut avoir un effet immédiat pour détecter des bugs mais surtout elle a un effet à long terme qui facilite la détection d'effets de bords lors de modifications.
Les cas de tests sont regroupés dans des classes Java qui contiennent une ou plusieurs méthodes de tests. Les cas de tests peuvent être exécutés individuellement ou sous la forme de suites de tests.
JUnit permet le développement incrémental d'une suite de tests.
Avec JUnit, l'unité de test est une classe dédiée qui regroupe des cas de tests. Ces cas de tests exécutent les tâches suivantes :
JUnit est particulièrement adapté pour être utilisé avec la méthode eXtreme Programming puisque cette méthode préconise, entre autres, l'automatisation des tâches de tests unitaires qui ont été définies avant l'écriture du code.
La version utilisée dans ce chapitre est la 3.8.1 sauf dans la section dédiée à la version 4 de JUnit.
La page officielle est à l'url : https://junit.org/.
La dernière version de JUnit peut être téléchargée sur le site junit.org/. Pour l'installer, il suffit de décompresser l'archive dans un répertoire du système.
Pour pouvoir utiliser JUnit, il faut ajouter le fichier junit.jar au classpath.
Ce chapitre contient plusieurs sections :
L'exemple utilisé dans cette section est la classe suivante :
Exemple : |
public class MaClasse{
public static int calculer(int a, int b) {
int res = a + b;
if (a == 0){
res = b * 2;
}
if (b == 0) {
res = a * a;
}
return res;
}
}
Il faut suivre plusieurs étapes :
1) compiler cette classe : javac MaClasse.java
2) écrire une classe qui va contenir les différents tests à réaliser par JUnit. L'exemple est volontairement simpliste en ne définissant qu'un seul cas de tests.
Exemple : |
import junit.framework.*;
public class MaClasseTest extends TestCase{
public void testCalculer() throws Exception {
assertEquals(2,MaClasse.calculer(1,1));
}
}
3) compiler cette classe avec le fichier junit.jar qui doit être dans le classpath.
4) enfin, appeler JUnit pour qu'il exécute la séquence de tests.
Exemple : |
java -cp junit.jar;. junit.textui.TestRunner MaClasseTest
C:\java\testjunit>java -cp junit.jar;. junit.textui.TestRunner
MaClasseTest
.
Time: 0,01
OK (1 test)
Attention : le respect de la casse dans le nommage des méthodes de tests est très important. Les méthodes de tests doivent obligatoirement commencer par test en minuscule car JUnit utilise l'introspection pour déterminer les méthodes à exécuter.
Exemple : |
import junit.framework.*;
public class MaClasseTest extends TestCase{
public void TestCalculer() throws Exception {
assertEquals(2,MaClasse.calculer(1,1));
}
}
L'utilisation de cette classe avec JUnit produit le résultat suivant :
Résultat : |
C:\java\testjunit>java -cp junit.jar;. junit.textui.TestRunner
MaClasseTest
.F
Time: 0,01
There was 1 failure:
1) warning(junit.framework.TestSuite$1)junit.framework.AssertionFailedError: No
tests found in MaClasseTest
FAILURES!!!
Tests run: 1, Failures: 1, Errors: 0
JUnit propose un framework pour écrire les classes de tests.
Un test est une classe qui hérite de la classe TestCase. Par convention le nom de la classe de test est composé du nom de la classe suivi de Test.
Chaque cas de tests fait l'objet d'une méthode dans la classe de tests. Le nom de ces méthodes doit obligatoirement commencer par le préfixe test.
Chacune de ces méthodes contient généralement des traitements en trois étapes :
Il est important de se souvenir lors de l'écriture de cas de tests que ceux-ci doivent être indépendants les uns des autres. JUnit ne garantit pas l'ordre d'exécution des cas de tests puisque ceux-ci sont obtenus par introspection.
Toutes les classes de tests avec JUnit héritent de la classe Assert.
Pour écrire les cas de tests, il faut écrire une classe qui étende la classe junit.framework.TestCase. Le nom de cette classe est le nom de la classe à tester suivi par "Test".
Remarque : dans la version 3.7 de JUnit, une classe de tests doit obligatoirement posséder un constructeur qui attend un objet de type String en paramètre.
Exemple : |
import junit.framework.*;
public class MaClasseTest extends TestCase{
public MaClasseTest(String testMethodName) {
super(testMethodName);
}
public void testCalculer() throws Exception {
fail("Cas de tests a ecrire");
}
}
Dans cette classe, il faut écrire une méthode dont le nom commence par "test" en minuscule suivi du nom du cas de tests (généralement le nom de la méthode à tester). Chacune de ces méthodes doit avoir les caractéristiques suivantes :
Par introspection, JUnit va automatiquement rechercher toutes les méthodes qui respectent cette convention. Le respect de ces règles est donc important pour une bonne exécution des tests par JUnit.
Chaque classe de tests doit avoir obligatoirement au moins une méthode de test sinon une erreur est remontée par JUnit.
JUnit recherche, par introspection, les méthodes qui débutent par test, n'ont aucun paramètre et ne retourne aucune valeur. Ces méthodes peuvent lever des exceptions qui sont automatiquement capturées par JUnit qui remonte alors une erreur et donc un échec du cas de tests.
Dès qu'un test échoue, l'exécution de la méthode correspondante est interrompue et JUnit passe à la méthode suivante.
La classe suivante sera utilisée dans les exemples de cette section :
Exemple : |
public class MaClasse2{
private int a;
private int b;
public MaClasse2(int a, int b) {
this.a = a;
this.b = b;
}
public int getA() {
return a;
}
public int getB() {
return b;
}
public void setA(int unA) {
this.a = unA;
}
public void setB(int unB) {
this.b = unB;
}
public int calculer() {
int res = a + b;
if (a == 0){
res = b * 2;
}
if (b == 0) {
res = a * a;
}
return res;
}
public int sommer() throws IllegalStateException {
if ((a == 0) && (b==0)) {
throw new IllegalStateException("Les deux valeurs sont nulles");
}
return a+b;
}
}
Avec JUnit, la plus petite unité de tests est l'assertion dont le résultat de l'expression booléenne indique un succès ou une erreur.
Les cas de tests utilisent des affirmations (assertion en anglais) sous la forme de méthodes nommées assertXXX() proposées par le framework. Il existe de nombreuses méthodes de ce type qui sont héritées de la classe junit.framework.Assert :
Méthode |
Rôle |
assertEquals() |
Vérifier l'égalité de deux valeurs de type primitif ou objet (en utilisant la méthode equals()). Il existe de nombreuses surcharges de cette méthode pour chaque type primitif, pour un objet de type Object et pour un objet de type String |
assertFalse() |
Vérifier que la valeur fournie en paramètre est fausse |
assertNull() |
Vérifier que l'objet fourni en paramètre soit null |
assertNotNull() |
Vérifier que l'objet fourni en paramètre ne soit pas null |
assertSame() |
Vérifier que les deux objets fournis en paramètre font référence à la même entité Exemples identiques : assertSame("Les deux objets sont identiques", obj1, obj2); assertTrue("Les deux objets sont identiques ", obj1 == obj2); |
assertNotSame() |
Vérifier que les deux objets fournis en paramètre ne font pas référence à la même entité |
assertTrue() |
Vérifier que la valeur fournie en paramètre est vraie |
Bien qu'il soit possible de n'utiliser que la méthode assertTrue(), les autres méthodes assertXXX() facilitent l'expression des conditions de tests.
Chacune de ces méthodes possède une version surchargée qui accepte un paramètre supplémentaire sous la forme d'une chaîne de caractères indiquant un message qui sera affiché en cas d'échec du cas de tests. Le message devrait décrire le cas de tests évalué à true.
L'utilisation de cette version surchargée est recommandée car elle facilite l'exploitation des résultats des cas de tests.
Exemple : |
import junit.framework.*;
public class MaClasse2Test extends TestCase{
public void testCalculer() throws Exception {
MaClasse2 mc = new MaClasse2(1,1);
assertEquals(2,mc.calculer());
}
}
L'ordre des paramètres contenant la valeur attendue et la valeur obtenue est important pour avoir un message d'erreur fiable en cas d'échec du cas de tests. Quelle que soit la surcharge utilisée l'ordre des deux valeurs à tester est toujours le même : c'est toujours la valeur attendue qui précède la valeur courante.
La méthode fail() permet de forcer le cas de tests à échouer. Une version surchargée permet de préciser un message qui sera affiché.
Il est aussi souvent utile lors de la définition des cas de tests de tester si une exception est levée lors de l'exécution des traitements.
Exemple : |
import junit.framework.*;
public class MaClasse2Test extends TestCase{
public void testSommer() throws Exception {
MaClasse2 mc = new MaClasse2(0,0);
mc.sommer();
}
}
Résultat : |
C:\>java -cp junit.jar;. junit.textui.TestRunner MaClasse2Test
.E
Time: 0,01
There was 1 error:
1) testSommer(MaClasse2Test)java.lang.IllegalStateException: Les deux valeurs so
nt nulles
at MaClasse2.sommer(MaClasse2.java:42)
at MaClasse2Test.testSommer(MaClasse2Test.java:31)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.
java:39)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAcces
sorImpl.java:25)
FAILURES!!!
Tests run: 2, Failures: 0, Errors: 1
Avec JUnit, pour réaliser de tels cas de tests, il suffit d'appeler la méthode avec les conditions qui doivent lever une exception, d'encapsuler cet appel dans un bloc try/catch et d'appeler la méthode fail() si l'exception désirée n'est pas levée.
Exemple : |
import junit.framework.*;
public class MaClasse2Test extends TestCase{
public void testSommer() throws Exception {
MaClasse2 mc = new MaClasse2(1,1);
// cas de test 1
assertEquals(2,mc.sommer());
// cas de test 2
try {
mc.setA(0);
mc.setB(0);
mc.sommer();
fail("Une exception de type IllegalStateException aurait du etre levee");
} catch (IllegalStateException ise) {
}
}
}
Il est fréquent que les cas de tests utilisent une instance d'un même objet ou nécessitent l'usage de ressources particulières telles qu'une instance d'une classe pour l'accès à une base de données par exemple.
Pour réaliser ces opérations de création et de destruction d'objets, la classe TestCase propose les méthodes setUp() et tearDown() qui sont respectivement appelées avant et après l'appel de chaque méthode contenant un cas de tests.
Il suffit simplement de redéfinir ces deux méthodes en fonction de ses besoins.
Cette section va tester le bean ci-dessous :
Exemple : |
package fr.jmdoudoux.dej.junit;
public class Personne {
private String nom;
private String prenom;
public Personne() {
super();
}
public Personne(String nom, String prenom) {
super();
this.nom = nom;
this.prenom = prenom;
}
public String getNom() {
return nom;
}
public void setNom(String nom) {
this.nom = nom;
}
public String getPrenom() {
return prenom;
}
public void setPrenom(String prenom) {
this.prenom = prenom;
}
}
Le plus simple est de définir un membre privé du type dont on a besoin et de créer une instance de ce type dans la méthode setUp().
Il est important de se souvenir que la méthode setUp() est invoquée systématiquement avant l'appel de chaque méthode de tests. Sa mise en oeuvre n'est donc requise que si toutes les méthodes de tests ont besoin de créer une instance d'un même type ou d'exécuter un même traitement.
Exemple : |
package fr.jmdoudoux.dej.junit;
import junit.framework.TestCase;
public class PersonneTest extends TestCase {
private Personne personne;
public PersonneTest(String name) {
super(name);
}
protected void setUp() throws Exception {
super.setUp();
personne = new Personne("nom1","prenom1");
}
protected void tearDown() throws Exception {
super.tearDown();
personne = null;
}
public void testPersonne() {
assertNotNull("L'instance n'est pas créée", personne);
}
public void testGetNom() {
assertEquals("Le nom est incorrect", "nom1", personne.getNom());
}
public void testSetNom() {
personne.setNom("nom2");
assertEquals("Le nom est incorrect", "nom2", personne.getNom());
}
public void testGetPrenom() {
assertEquals("Le prenom est incorrect", "prenom1", personne.getPrenom());
}
public void testSetPrenom() {
personne.setPrenom("prenom2");
assertEquals("Le prenom est incorrect", "prenom2", personne.getPrenom());
}
}
Ceci évite de créer l'instance dans chaque méthode de tests et simplifie donc l'écriture des cas de tests.
Dans l'exemple la méthode tearDown() remet à null l'instance créée : ceci n'est pas une obligation d'autant que le temps des traitements réalisés durant les tests est normalement négligeable. La méthode tearDown() peut cependant avoir un grand intérêt pour, par exemple, libérer des ressources comme une connexion à une base de données initialisée dans la méthode setUp().
Pour des besoins particuliers, il peut être nécessaire d'exécuter du code une seule fois avant l'exécution des cas de tests et/ou d'exécuter du code une fois tous les cas de tests exécutés.
JUnit propose pour cela la classe junit.Extensions.TestSetup qui propose la mise en oeuvre du design pattern décorateur.
Exemple : |
package fr.jmdoudoux.dej.junit;
import junit.extensions.TestSetup;
import junit.framework.Test;
import junit.framework.TestCase;
import junit.framework.TestSuite;
public class PersonneTest extends TestCase {
private Personne personne;
public PersonneTest(String name) {
super(name);
}
protected void setUp() throws Exception {
super.setUp();
personne = new Personne("nom1", "prenom1");
}
protected void tearDown() throws Exception {
super.tearDown();
personne = null;
}
...
public static Test suite() {
TestSetup setup = new TestSetup(new TestSuite(PersonneTest.class)) {
protected void setUp() throws Exception {
// code execute une seule fois avant l'exécution des cas de tests
System.out
.println("Appel de la methode setUp() de la classe de tests");
}
protected void tearDown() throws Exception {
// code execute une seule fois après l'exécution de tous les cas de tests
System.out
.println("Appel de la methode tearDown() de la classe de tests");
}
};
return setup;
}
public static void main(String[] args) {
junit.textui.TestRunner.run(suite());
}
}
Dans l'exemple ci-dessus, les méthodes setUp() et tearDown() de la classe PersonneTest seront toujours invoquées respectivement avant et après chaque exécution d'un cas de tests.
Il est fréquent qu'une méthode puisse lever une ou plusieurs exceptions durant son exécution. Il faut prévoir des cas de tests pour vérifier que dans les conditions adéquates une exception attendue est bien levée.
Exemple : |
package fr.jmdoudoux.dej.junit;
public class Personne {
private String nom;
private String prenom;
public Personne() {
super();
}
public Personne(String nom, String prenom) {
super();
this.nom = nom;
this.prenom = prenom;
}
public String getNom() {
return nom;
}
public void setNom(String nom) {
if (nom == null) {
throw new IllegalArgumentException("la propriété nom ne peut pas être null");
}
this.nom = nom;
}
public String getPrenom() {
return prenom;
}
public void setPrenom(String prenom) {
this.prenom = prenom;
}
}
Pour effectuer la vérification de la levée d'une exception, il faut inclure l'invocation de la méthode dans un bloc try/catch et faire appel à la méthode fail() si l'exception n'est pas levée.
Exemple : |
package fr.jmdoudoux.dej.junit;
import junit.framework.TestCase;
public class PersonneTest extends TestCase {
private Personne personne;
public PersonneTest(String name) {
super(name);
}
protected void setUp() throws Exception {
super.setUp();
personne = new Personne("nom1","prenom1");
}
protected void tearDown() throws Exception {
super.tearDown();
personne = null;
}
public void testPersonne() {
assertNotNull("L'instance n'est pas créée", personne);
}
public void testGetNom() {
assertEquals("Le nom est incorrect", "nom1", personne.getNom());
}
public void testSetNom() {
personne.setNom("nom2");
assertEquals("Le nom est incorrect", "nom2", personne.getNom());
try {
personne.setNom(null);
fail("IllegalArgumentException non levée avec la propriété nom à null");
} catch (IllegalArgumentException iae) {
// ignorer l'exception puisque le test est OK (l'exception est levée)
}
}
public void testGetPrenom() {
assertEquals("Le prenom est incorrect", "prenom1", personne.getPrenom());
}
public void testSetPrenom() {
personne.setPrenom("prenom2");
assertEquals("Le prenom est incorrect", "prenom2", personne.getPrenom());
}
}
Attention : une erreur courante lorsque l'on code ses premiers tests unitaires est d'inclure les invocations de méthodes dans des blocs try/catch. Leur utilisation doit être uniquement réservée aux situations telles que celle de l'exemple précédant. Dans tous les autres cas, il faut laisser l'exception se propager : dans ce cas, JUnit va automatiquement reporter un échec du test. Il est en particulier inutile d'utiliser un bloc try/catch et de faire appel à la méthode fail() dans le catch puisque JUnit le fait déjà.
Il est possible de définir une classe de base qui servira de classe mère à d'autres classes de tests notamment en leur fournissant des fonctionnalités communes.
JUnit n'impose pas qu'une classe de tests dérive directement de la classe TestCase. Ceci est particulièrement pratique lorsque l'on souhaite que certaines initialisations ou certains traitements soit systématiquement exécutés (exemple chargement d'un fichier de configuration, ...).
Il est par exemple possible de faire des initialisations dans le constructeur de la classe mère et d'invoquer ce constructeur dans les constructeurs des classes filles.
JUnit propose trois applications différentes nommées TestRunner pour exécuter les tests en mode ligne de commande ou application graphique :
Quelle que soit l'application utilisée, les entités suivantes doivent être incluses dans le classpath :
Suite à l'exécution d'un cas de tests, celui-ci peut avoir un des trois états suivants :
L'échec d'un seul cas de tests entraîne l'échec du test complet.
L'échec d'un cas de tests peut avoir plusieurs origines :
L'utilisation de l'application console nécessite quelques paramètres :
Résultat : |
C:\>java -cp junit.jar;. junit.textui.TestRunner MaClasseTest
Le seul paramètre obligatoire est le nom de la classe de tests. Celle-ci doit obligatoirement être sous la forme pleinement qualifiée si elle appartient à un package.
Résultat : |
C:\>java -cp junit.jar;. junit.textui.TestRunner fr.jmdoudoux.dej.junit.MaClasseTest
Il est possible de faire appel au TestRunner dans une application en utilisant sa méthode run() à laquelle on passe en paramètre un objet de type Class qui encapsule la classe de tests à exécuter.
Résultat : |
public class TestJUnit1 {
public static void main(String[] args) {
junit.textui.TestRunner.run(MaClasseTest.class);
}
}
Le TestRunner affiche le résultat de l'exécution des tests dans la console.
La première ligne contient un caractère point pour chaque test exécuté. Lorsque de nombreux tests sont exécutés cela permet de suivre la progression.
Le temps total d'exécution en secondes est ensuite affiché sur la ligne "Time:"
Enfin, un résumé des résultats de l'exécution est affiché.
Résultat : |
.
Time: 0,078
OK (1 test)
En cas d'erreur, la première ligne contient un F à la suite du caractère point correspondant au cas de tests en échec.
Le résumé de l'exécution affiche le détail de chaque cas de tests qui a échoué.
Résultat : |
.F
Time: 0,063
There was 1 failure:
1) testCalculer(fr.jmdoudoux.dej.junit.MaClasseTest)junit.framework.AssertionFailedError:
expected:<3> but was:<2>
at fr.jmdoudoux.dej.junit.MaClasseTest.testCalculer(MaClasseTest.java:9)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(Unknown Source)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(Unknown Source)
at fr.jmdoudoux.dej.junit.MaClasseTest.main(MaClasseTest.java:13)
FAILURES!!!
Tests run: 1, Failures: 1, Errors: 0
Les cas en échec (failures) correspondent à une vérification faite par une méthode assertXXX() qui a échoué.
Les cas en erreur (errors) correspondent à la levée inattendue d'une exception lors de l'exécution du cas de tests.
Pour utiliser des classes de tests avec ces applications graphiques, il faut obligatoirement que les classes de tests et toutes celles dont elles dépendent soient incluses dans le CLASSPATH. Elles doivent obligatoirement être sous la forme de fichier .class non inclus dans un fichier jar.
Exemple : |
C:\java\testjunit>java -cp junit.jar;. junit.swingui.TestRunner MaClasseTest
Il suffit de cliquer sur le bouton "Run" pour lancer l'exécution des tests.
C:\java\testjunit>java -cp junit.jar;. junit.awtui.TestRunner MaClasseTest
La case à cocher "Reload classes every run" indique à JUnit de recharger les classes à chaque exécution. Ceci est très pratique car cela permet de modifier les classes tout en laissant l'application de tests ouverte.
Si un ou plusieurs tests échouent la barre de résultats n'est plus verte mais rouge. Dans ce cas, le nombre d'erreurs et d'échecs est affiché ainsi que leur liste complète. Il suffit d'en sélectionner un pour obtenir le détail de la raison du problème.
Il est aussi possible de ne réexécuter que le cas sélectionné.
Il est possible de définir une classe main() dans une classe de tests qui va se charger d'exécuter les tests.
Exemple : |
public class MaClasseTest extends TestCase {
...
public static void main(String[] args) {
junit.textui.TestRunner.run(new TestSuite(MaClasseTest.class));
}
}
JUnit propose la classe junit.extensions.RepeatedTest qui permet d'exécuter plusieurs fois la même suite de tests.
Le constructeur de cette classe attend en paramètre une instance de la suite de tests et le nombre de répétitions de l'exécution.
Exemple : |
package fr.jmdoudoux.dej.junit;
import junit.extensions.RepeatedTest;
import junit.framework.Test;
import junit.framework.TestCase;
import junit.framework.TestSuite;
public class PersonneTest extends TestCase {
private Personne personne;
public PersonneTest(String name) {
super(name);
}
...
public static Test suite() {
return new RepeatedTest(new TestSuite(PersonneTest.class), 5);
}
public static void main(String[] args) {
junit.textui.TestRunner.run(suite());
}
}
JUnit propose la classe junit.extensions.ActiveTestSuite qui permet d'exécuter plusieurs suites de tests chacune dans un thread dédié. Ainsi l'exécution des suites de tests se fait de façon concurrente.
Exemple : |
package fr.jmdoudoux.dej.junit;
import junit.extensions.ActiveTestSuite;
import junit.framework.Test;
import junit.framework.TestCase;
import junit.framework.TestSuite;
public class PersonneTest extends TestCase {
private Personne personne;
public PersonneTest(String name) {
super(name);
}
...
public static Test suite() {
TestSuite suite = new ActiveTestSuite();
suite.addTest(new TestSuite(PersonneTest.class));
suite.addTest(new TestSuite(PersonneTest.class));
suite.addTest(new TestSuite(PersonneTest.class));
return suite;
}
public static void main(String[] args) {
junit.textui.TestRunner.run(suite());
}
}
L'ensemble de la suite de tests ne se termine que lorsque tous les threads sont terminés.
Même si cela n'est pas recommandé, la classe ActiveTestSuite peut être utilisée comme un outil de charge rudimentaire. Il est ainsi possible de combiner l'utilisation des classes ActiveTestsuite et RepeatedTest.
Exemple : |
package fr.jmdoudoux.dej.junit;
import junit.extensions.ActiveTestSuite;
import junit.extensions.RepeatedTest;
import junit.framework.Test;
import junit.framework.TestCase;
import junit.framework.TestSuite;
public class PersonneTest extends TestCase {
private Personne personne;
public PersonneTest(String name) {
super(name);
}
...
public static Test suite() {
TestSuite suite = new ActiveTestSuite();
suite.addTest(new RepeatedTest(new TestSuite(PersonneTest.class), 10));
suite.addTest(new RepeatedTest(new TestSuite(PersonneTest.class), 20));
return suite;
}
public static void main(String[] args) {
junit.textui.TestRunner.run(suite());
}
}
Les suites de tests permettent de regrouper plusieurs tests dans une même classe. Ceci permet l'automatisation de l'ensemble des tests inclus dans la suite et de préciser leur ordre d'exécution.
Pour créer une suite, il suffit de créer une classe de type TestSuite et d'appeler la méthode addTest() pour chaque classe de tests à ajouter. Celle-ci attend en paramètre une instance de la classe de tests qui sera ajoutée à la suite. L'objet de type TestSuite ainsi créé doit être renvoyé par une méthode dont la signature est obligatoirement public static Test suite(). Celle-ci sera appelée par introspection par le TestRunner.
Il peut être pratique de définir une méthode main() dans la classe qui encapsule la suite de tests pour pouvoir exécuter le TestRunner de la console en exécutant directement la méthode statique Run(). Ceci évite de lancer JUnit sur la ligne de commandes.
Exemple : |
import junit.framework.*;
public class ExecuterLesTests {
public static Test suite() {
TestSuite suite = new TestSuite("Tous les tests");
suite.addTestSuite(MaClasseTest.class);
suite.addTestSuite(MaClasse2Test.class);
return suite;
}
public static void main(String args[]) {
junit.textui.TestRunner.run(suite());
}
}
Deux versions surchargées des constructeurs permettent de donner un nom à la suite de tests.
Un constructeur de la classe TestSuite permet de créer automatiquement par introspection une suite de tests contenant tous les tests de la classe fournie en paramètre.
Exemple : |
import junit.framework.*;
public class ExecuterLesTests2 {
public static Test suiteDeTests() {
TestSuite suite = new TestSuite(MaClasseTest.class,"Tous les tests");
return suite;
}
public static void main(String args[]) {
junit.textui.TestRunner.run(suiteDeTests());
}
}
Pour éviter d'avoir à gérer une suite de tests, il est possible d'utiliser la tâche Ant optionnelle junit qui exécutera un ensemble de cas de tests en fonction d'un filtre sur le nom des classes.
Exemple : |
...
<junit printsummary="yes" haltonfailure="yes">
...
<batchtest fork="yes">
<fileset dir="${src.dir}">
<include name="**/Test*.java" />
</fileset>
</batchtest>
</junit>
...
Le détail de la mise en oeuvre de JUnit avec Ant est couvert dans la section suivante.
La méthode addTestSuite() permet d'ajouter à une suite une autre suite.
L'automatisation des tests fait par JUnit au moment de la génération de l'application est particulièrement pratique. Ainsi Ant propose une tâche optionnelle dédiée nommée junit pour exécuter un TestRunner dans la console.
Pour pouvoir utiliser cette tâche, les fichiers junit.jar (fourni avec JUnit) et optional.jar (fourni avec Ant) doivent être accessibles dans le CLASSSPATH.
Cette tâche possède plusieurs attributs dont aucun n'est obligatoire. Les principaux sont :
Attribut |
Rôle |
Valeur par défaut |
printsummary |
affiche un résumé statistique de l'exécution de chaque test |
off |
fork |
exécution du TestRunner dans une JVM séparée |
off |
haltonerror |
arrêt de la génération en cas d'erreur |
off |
haltonfailure |
arrêt de la génération en cas d'échec d'un test |
off |
outfile |
base du nom du fichier qui va contenir les résultats de l'exécution |
La tâche <junit> peut avoir les éléments fils suivants : <jvmarg>, <sysproperty>, <env>, <formatter>, <test>, <batchtest>.
L'élément <formatter> permet de préciser le format de sortie des résultats de l'exécution des tests. Il possède l'attribut type qui précise le format (les valeurs possibles sont : xml, plain ou brief) et l'attribut usefile qui précise si les résultats doivent être envoyés dans un fichier (les valeurs possibles sont : true ou false).
L'élément <test> permet de préciser un cas de tests simple ou une suite de tests selon le contenu de la classe précisée par l'attribut name. Cet élément possède de nombreux attributs et il est possible d'utiliser un élément fils de type <formatter> pour définir le format de sortie du test.
L'élément <batchtest> permet de réaliser toute une série de tests. Cet élément possède de nombreux attributs et il est possible d'utiliser un élément fils de type <formatter> pour définir le format de sortie des tests. Les différentes classes dont les tests sont à exécuter sont précisées par un élément fils <fileset>.
La tâche <junit> doit être exécutée après la compilation des classes à tester.
Exemple : extrait d'un fichier build.xml pour Ant |
<?xml version="1.0" encoding="ISO-8859-1" ?>
<project name="TestAnt1" default="all">
<description>Génération de l'application</description>
<property name="bin" location="bin"/>
<property name="src" location="src"/>
<property name="build" location="build"/>
<property name="doc" location="${build}/doc"/>
<property name="lib" location="${build}/lib"/>
<property name="junit_path" value="junit.jar"/>
...
<target name="test" depends="compil" description="Executer les tests avec JUnit">
<junit fork="yes" haltonerror="true" haltonfailure="on" printsummary="on">
<formatter type="plain" usefile="false" />
<test name="ExecuterLesTests"/>
<classpath>
<pathelement location="${bin}"/>
<pathelement location="${junit_path}"/>
</classpath>
</junit>
</target>
...
</project>
Cet exemple exécute les tests de la suite de tests encapsulée dans la classe ExecuterLesTests
JUnit version 4 est une évolution majeure depuis les quelques années d'utilisation de la version 3.8.
Un des grands bénéfices de cette version est l'utilisation des annotations introduites dans Java 5. La définition des cas de tests et des tests ne se fait donc plus sur des conventions de nommage et sur l'introspection mais sur l'utilisation d'annotations ce qui facilite la rédaction des cas de tests.
Une compatibilité descendante est assurée avec les suites de tests de JUnit 3.8.
JUnit 4 requiert une version 5 ou ultérieure de Java.
Le nom du package des classes de JUnit est différent entre la version 3 et 4 :
Une classe de tests n'a plus l'obligation d'étendre la classe TestCase sous réserve d'utiliser les annotations définies par JUnit et d'utiliser des imports static sur les méthodes de la classe org.junit.Assert.
Exemple : |
package fr.jmdoudoux.dej.junit4;
import org.junit.*;
import static org.junit.Assert.*;
public class MaClasse {
}
Les méthodes contenant les cas de tests n'ont plus l'obligation d'utiliser la convention de nommage qui imposait de préfixer le nom des méthodes avec test.
Avec JUnit 4, il suffit d'annoter la méthode avec l'annotation @Test.
Il est ainsi possible d'utiliser n'importe quelle méthode comme cas de tests simplement en utilisant l'annotation @Test.
Exemple : |
@Test
public void getNom() {
assertEquals("Le nom est incorrect", "nom1", personne.getNom());
}
Ceci permet d'utiliser le nom de méthode que l'on souhaite. Il est cependant conseillé de définir et d'utiliser une convention de nommage qui facilitera l'identification des classes de tests et des cas de tests. Il est par exemple possible de maintenir les conventions de nommage de JUnit 3.
L'annotation @Ignore permet de demander au framework d'ignorer un cas de tests. Les cas de tests dans ce cas sont marqués avec la lettre I lors de leur exécution en mode console.
Attention : l'utilisation de l'annotation @Ignore devrait être temporaire et justifiée. Son utilisation ne doit pas devenir une solution à certains problèmes.
JUnit 4 inclut deux nouvelles surcharges de la méthode assertEquals() qui permettent de comparer deux tableaux d'objets. La comparaison se fait sur le nombre d'occurrences dans les tableaux et sur l'égalité de chaque objet d'un tableau dans l'autre tableau.
JUnit 3 imposait une redéfinition des méthodes setUp() et TearDown() pour définir des traitements exécutés systématiquement avant et après chaque cas de tests.
JUnit 4 propose simplement d'annoter la méthode exécutée avant avec l'annotation @Before et la méthode exécutée après avec l'annotation @After.
Exemple : |
@Before
public void initialiser() throws Exception {
personne = new Personne("nom1","prenom1");
}
@After
public void nettoyer() throws Exception {
personne = null;
}
Il est possible d'annoter une ou plusieurs méthodes avec @Before ou @After. Dans ce cas, toutes les méthodes seront invoquées au moment correspondant à leur annotation.
Il n'est pas nécessaire d'invoquer explicitement les méthodes annotées avec @Before et @After d'une classe mère. Tant que ces méthodes ne sont pas redéfinies, elles seront automatiquement invoquées lors de l'exécution des tests :
JUnit 4 propose simplement d'annoter une ou plusieurs méthodes exécutées avant l'exécution du premier cas de tests avec l'annotation @BeforeClass et une ou plusieurs méthodes exécutées après l'exécution de tous les cas de tests de la classe avec l'annotation @AfterClass.
Ces initialisations peuvent être très utiles notamment pour des connexions coûteuses à des ressources qu'il est préférable de ne réaliser qu'une seule fois plutôt qu'à chaque cas de tests. Ceci peut contribuer à améliorer les performances lors de l'exécution des tests.
Avec JUnit 3, pour vérifier la levée d'une exception dans un cas de tests, il faut entourer l'appel du traitement dans un bloc try/catch et invoquer la méthode fail() à la fin du bloc try.
JUnit 4 propose une annotation pour faciliter la vérification de la lever d'une exception.
L'attribut expected de l'annotation @Test attend comme valeur la classe de l'exception qui devrait être levée.
Exemple : |
@Test(expected=IllegalArgumentException.class)
public void setNom() {
personne.setNom("nom2");
assertEquals("Le nom est incorrect", "nom2", personne.getNom());
personne.setNom(null);
}
Si lors de l'exécution du test l'exception du type précisée n'est pas levée (aucune exception levée ou une autre exception est levée) alors le test échoue.
Attention : l'utilisation de l'annotation ne permet que de vérifier que l'exception est levée. Pour vérifier des propriétés de l'exception, il est nécessaire d'utiliser le mécanisme utilisé avec JUnit 3 pour capturer l'exception et ainsi avoir accès aux membres de son instance.
Les applications graphiques AWT et Swing permettant l'exécution et l'affichage des résultats des cas de tests ne sont plus fournies avec JUnit 4.
JUnit laisse le soin de cette restitution aux IDE qui intègrent JUnit 4 comme par exemple Eclipse.
Une autre grande différence dans la façon d'exécuter les cas de tests avec JUnit 4 concerne le fait qu'il n'y a plus de différence entre un test échoué (échec d'une méthode assert() ou appel à la méthode fail()) et un test en erreur (une exception inattendue est levée).
Lors de l'exécution, si un avertissement de type "AssertionFailedError: No tests found in XXX" est fourni par JUnit c'est qu'aucun cas de tests n'est fourni dans la classe (aucune méthode n'est annotée avec l'annotation @Test).
Dans une classe de tests, il est toujours possible de définir une méthode main() qui permette de demander l'exécution des cas de tests de la classe. Il faut invoquer la méthode main() de la classe org.junit.runner.JUnitCore.
Exemple : |
package fr.jmdoudoux.dej.junit4;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
public class PersonneTest {
private Personne personne;
@Before
public void initialiser() throws Exception {
personne = new Personne("nom1","prenom1");
}
@After
public void nettoyer() throws Exception {
personne = null;
}
@Test
public void personne() {
assertNotNull("L'instance n'est pas créée", personne);
}
...
public static void main(String[] args) {
org.junit.runner.JUnitCore.main("fr.jmdoudoux.dej.junit4.PersonneTest");
}
}
La section ci-dessous propose une classe qui encapsule des tests avec JUnit 3 et une classe qui propose des fonctionnalités équivalentes en JUnit 4.
Exemple avec JUnit 3 : |
package fr.jmdoudoux.dej.junit;
import junit.framework.TestCase;
public class PersonneTest extends TestCase {
private Personne personne;
public PersonneTest(String name) {
super(name);
}
protected void setUp() throws Exception {
super.setUp();
personne = new Personne("nom1","prenom1");
}
protected void tearDown() throws Exception {
super.tearDown();
personne = null;
}
public void testPersonne() {
assertNotNull("L'instance n'est pas créée", personne);
}
public void testGetNom() {
assertEquals("Le nom est incorrect", "nom1", personne.getNom());
}
public void testSetNom() {
personne.setNom("nom2");
assertEquals("Le nom est incorrect", "nom2", personne.getNom());
}
public void testGetPrenom() {
assertEquals("Le prenom est incorrect", "prenom1", personne.getPrenom());
}
public void testSetPrenom() {
personne.setPrenom("prenom2");
assertEquals("Le prenom est incorrect", "prenom2", personne.getPrenom());
}
}
Exemple avec JUnit 4 : |
package fr.jmdoudoux.dej.junit4;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
public class PersonneTest {
private Personne personne;
@Before
public void initialiser() throws Exception {
personne = new Personne("nom1","prenom1");
}
@After
public void nettoyer() throws Exception {
personne = null;
}
@Test
public void personne() {
assertNotNull("L'instance n'est pas créée", personne);
}
@Test
public void getNom() {
assertEquals("Le nom est incorrect", "nom1", personne.getNom());
}
@Test(expected=IllegalArgumentException.class)
public void setNom() {
personne.setNom("nom2");
assertEquals("Le nom est incorrect", "nom2", personne.getNom());
personne.setNom(null);
}
@Test
public void getPrenom() {
assertEquals("Le prenom est incorrect", "prenom1", personne.getPrenom());
}
@Test
public void setPrenom() {
personne.setPrenom("prenom2");
assertEquals("Le prenom est incorrect", "prenom2", personne.getPrenom());
}
}
JUnit 4 propose une fonctionnalité rudimentaire pour vérifier qu'un cas de tests s'exécute dans un temps maximum donné.
L'attribut timeout de l'annotation @Test attend comme valeur un délai maximum d'exécution exprimé en millisecondes.
Exemple : |
package fr.jmdoudoux.dej.junit4;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
public class PersonneTest {
...
@Test(timeout=100)
public void compteur() {
for(long i = 0 ; i < 999999999; i++) { long a = i + 1; }
}
public static void main(String[] args) {
org.junit.runner.JUnitCore.main("fr.jmdoudoux.dej.junit4.PersonneTest");
}
}
Si le temps d'exécution du cas de tests est supérieur au temps fourni, alors le cas de tests échoue.
Résultat : |
JUnit version 4.3.1
......E
Time: 0,141
There was 1 failure:
1) compteur(fr.jmdoudoux.dej.junit4.PersonneTest)
java.lang.Exception: test timed out after 100 milliseconds
at org.junit.internal.runners.TestMethodRunner.runWithTimeout
(TestMethodRunner.java:68)
at org.junit.internal.runners.TestMethodRunner.run
(TestMethodRunner.java:43)
at org.junit.internal.runners.TestClassMethodsRunner.invokeTestMethod
(TestClassMethodsRunner.java:66)
at org.junit.internal.runners.TestClassMethodsRunner.run
(TestClassMethodsRunner.java:35)
at org.junit.internal.runners.TestClassRunner$1.runUnprotected
(TestClassRunner.java:42)
at org.junit.internal.runners.BeforeAndAfterRunner.runProtected
(BeforeAndAfterRunner.java:34)
at org.junit.internal.runners.TestClassRunner.run(TestClassRunner.java:52)
at org.junit.internal.runners.CompositeRunner.run(CompositeRunner.java:29)
at org.junit.runner.JUnitCore.run(JUnitCore.java:130)
at org.junit.runner.JUnitCore.run(JUnitCore.java:109)
at org.junit.runner.JUnitCore.run(JUnitCore.java:100)
at org.junit.runner.JUnitCore.runMain(JUnitCore.java:81)
at org.junit.runner.JUnitCore.main(JUnitCore.java:44)
at fr.jmdoudoux.dej.junit4.PersonneTest.main(PersonneTest.java:58)
FAILURES!!!
Tests run: 6, Failures: 1
|
Cette section sera développée dans une version future de ce document
|
Il est possible d'exécuter des tests JUnit 4 dans une application d'exécution de Tests JUnit 3.
Pour cela, il faut dans la classe de tests, ajouter une méthode suite() qui retourne un objet de type junit.framework.Test. Cette méthode instancie un objet de type JUnit4TestAdapter qui attend comme paramètre de son constructeur l'objet class de la classe de tests.
Exemple : |
package fr.jmdoudoux.dej.junit4;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
public class PersonneTest {
...
public static junit.framework.Test suite() {
return new JUnit4testAdapter(PersonneTest.class);
}
}
L'exécution nécessite tout de même une version 5 ou supérieure de Java.
Il est généralement préférable de n'avoir qu'un seul assert par test car un test ne devrait avoir qu'une seule raison d'échouer.
Exemple : |
package fr.jmdoudoux.dej;
public class SecuriteHelper {
public static boolean isMotDePasseValide(String mdp) {
boolean resultat = true;
if (mdp == null) {
resultat = false;
throw new IllegalArgumentException("le mot de passe n'est pas renseigne");
} else {
if (mdp.length() < 6 || mdp.length() > 15) {
resultat = false;
}
if (!mdp.matches(".*[a-zA-Z]*[0-9]*[a-zA-Z]")) {
resultat = false;
}
}
return resultat;
}
}
La méthode en exemple permet de valider un mot de passe en contrôlant quelques règles simples :
Il est possible d'écrire une classe de tests ne possédant qu'un seul cas de tests avec plusieurs asserts.
Exemple : |
package fr.jmdoudoux.dej;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import org.junit.Test;
public class SecurirteHelperTest {
@Test
public void testIsMotDePasseValide() {
try {
SecuriteHelper.isMotDePasseValide(null);
fail("Absence de la levee de l'exception IllegaleArgumentException");
} catch (IllegalArgumentException iae) {
// l'exception est levée
}
assertFalse("Le mot de passe est vide",
SecuriteHelper.isMotDePasseValide(""));
assertFalse("Le mot de passe est trop court",
SecuriteHelper.isMotDePasseValide("aaa"));
assertFalse("Le mot de passe est trop long",
SecuriteHelper.isMotDePasseValide("aaaaaaaaaaaaaaaaaaaaaaaaaaaa"));
assertFalse("Le mot de passe ne contient pas de chiffre",
SecuriteHelper.isMotDePasseValide("aaaaa"));
assertFalse("Le mot de passe contient un chiffre en derniere position",
SecuriteHelper.isMotDePasseValide("aaaaa6"));
assertTrue("Le mot de passe est valide",
SecuriteHelper.isMotDePasseValide("aAa6Aa"));
assertTrue("Le mot de passe est valide",
SecuriteHelper.isMotDePasseValide("a@aA6aa"));
assertTrue("Le mot de passe est valide",
SecuriteHelper.isMotDePasseValide("abc456def"));
}
Il est préférable d'écrire une méthode par cas de tests même si cela nécessite l'écriture de plus de code.
Exemple : |
package fr.jmdoudoux.dej;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import org.junit.Test;
public class SecurirteHelperTest {
@Test(expected=IllegalArgumentException.class)
public void testIsMotDePasseValideNull() {
SecuriteHelper.isMotDePasseValide(null);
}
@Test
public void testIsMotDePasseValideVide() {
assertFalse("Le mot de passe est vide",
SecuriteHelper.isMotDePasseValide(""));
}
@Test
public void testIsMotDePasseValideTropCourt() {
assertFalse("Le mot de passe est trop court",
SecuriteHelper.isMotDePasseValide("aaa"));
}
@Test
public void testIsMotDePasseValideTropLong() {
assertFalse("Le mot de passe est trop long",
SecuriteHelper.isMotDePasseValide("aaaaaaaaaaaaaaaaaaaaaaaaaaaa"));
}
@Test
public void testIsMotDePasseValideSansChiffre() {
assertFalse("Le mot de passe ne contient pas de chiffre",
SecuriteHelper.isMotDePasseValide("aaaaa"));
}
@Test
public void testIsMotDePasseValideChiffreEnDernier() {
assertFalse("Le mot de passe contient un chiffre en derniere position",
SecuriteHelper.isMotDePasseValide("aaaaa6"));
}
@Test
public void testIsMotDePasseValideAvecMinMaj() {
assertTrue("Le mot de passe est valide",
SecuriteHelper.isMotDePasseValide("aAa6Aa"));
}
@Test
public void testIsMotDePasseValideAvecArobase() {
assertTrue("Le mot de passe est valide",
SecuriteHelper.isMotDePasseValide("a@aA6aa"));
}
@Test
public void testIsMotDePasseValideStandard() {
assertTrue("Le mot de passe est valide",
SecuriteHelper.isMotDePasseValide("abc456def"));
}
}
En plus de s'assurer que tous les cas de tests sont exécutés même s'il y en a un qui échoue cela permet aussi de connaître plus précisément le nombre de cas de tests exécutés (10 au lieu de 1 dans l'exemple). Les cas de tests sont aussi plus simples donc plus maintenables.
Il est cependant possible d'utiliser plusieurs asserts dans un cas de tests si ceux-ci concernent un même cas fonctionnel.
Pour des cas plus concrets, il peut être nécessaire d'utiliser des méthodes de type setUp() ou TearDown() au besoin pour réduire la quantité de code nécessaire à la mise en place du contexte d'exécution de chaque cas de tests.
Développons en Java v 2.40 Copyright (C) 1999-2023 Jean-Michel DOUDOUX. |