Introduction au test logiciel/Tests unitaires/JUnit
Ici, nous allons présenter succinctement JUnit afin d'illustrer l'utilisation d'un framework de type xUnit. Même si on ne pratique pas Java au quotidien, cela permet de voir les principes généraux qu'on retrouve dans toutes les implémentations de ce framework. Nous n'allons pas nous étendre plus longtemps sur la question étant donné que de nombreux tutoriels sont disponibles sur la Toile pour les différents langages.
JUnit 4 requiert Java 1.5 car il utilise les annotations. JUnit 4 intègre JUnit 3 et peut donc lancer des tests écrits en JUnit 3.
Écrire un test
modifierAvec JUnit, on va créer une nouvelle classe pour chaque classe testée. On crée autant de méthodes que de tests indépendants : imaginez que les tests peuvent être passés dans n'importe quel ordre (i.e. les méthodes peuvent être appelées dans un ordre différent de celui dans lequel elles apparaissent dans le code source). Il n'y a pas de limite au nombre de tests que vous pouvez écrire. Néanmoins, on essaye généralement d'écrire au moins un test par méthode de la classe testée.
Pour désigner une méthode comme un test, il suffit de poser l'annotation @Test
.
import static org.junit.Assert.*;
import org.junit.Test;
public class StringTest {
@Test
public void testConcatenation() {
String foo = "abc";
String bar = "def";
assertEquals("abcdef", foo + bar);
}
@Test
public void testStartsWith() {
String foo = "abc";
assertTrue(foo.startsWith("ab"));
}
}
Les assertions
modifierVia import static org.junit.Assert.*;
, vous devez faire appel dans les tests aux méthodes statiques assertTrue, assertFalse, assertEquals, assertNull, fail, etc. en fournissant un message :
?
Votre environnement de développement devrait vous permettre de découvrir leurs signatures grâce à l'auto-complétion. À défaut, vous pouvez toutes les retrouver dans la documentation de l'API JUnit[1].
Attention à ne pas confondre les assertions JUnit avec « assert
». Ce dernier est un élément de base du langage Java[2]. Par défaut, les assertions Java sont ignorées par la JVM à moins de préciser -ea
au lancement de la JVM.
Lancer les tests
modifierPour lancer les tests, vous avez plusieurs possibilités selon vos préférences :
- La plus courante : lancer les tests depuis votre IDE (ex : alt + F6 dans NetBeans).
- Utiliser l'outil graphique.
- Lancer les tests en ligne de commande.
- Utiliser un système de construction logiciel (comme Ant ou Maven pour Java. Ex :
mvn test
pour tous les test,mvn -Dtest=MaClasseDeTest#MaMethodDeTest
test pour un seul).
Factoriser les éléments communs entre tests
modifierOn peut déjà chercher à factoriser les éléments communs à tous les tests d'une seule classe. Un test commence toujours par l'initialisation de quelques instances de formes différentes pour pouvoir tester les différents cas. C'est souvent un élément redondant des tests d'une classe, aussi, on peut factoriser tout le code d'initialisation commun à tous les tests dans une méthode spéciale, qui sera appelée avant chaque test pour préparer les données.
Si on considère l'exemple ci-dessus, cela donne :
import static org.junit.Assert.*;
import org.junit.Test;
import org.junit.*; //or import org.junit.Before; + import org.junit.After;
public class StringTest {
private String foo;
private String bar;
@Before // avec cette annotation, cette méthode sera appelée avant chaque test
public void setup() {
foo = "abc";
bar = "def";
}
@After
public void tearDown() {
// dans cet exemple, il n'y a rien à faire mais on peut,
// dans d'autres cas, avoir besoin de fermer une connexion
// à une base de données ou de fermer des fichiers
}
@Test
public void testConcatenation() {
assertEquals("abcdef", foo + bar);
}
@Test
public void testStartsWith() {
assertTrue(foo.startsWith("ab"));
}
}
Remarquons que la méthode annotée @Before, est exécutée avant chaque test, ainsi, chaque test est exécuté avec des données saines : celles-ci n'ont pu être modifiées par les autres tests.
On peut également souhaiter factoriser des éléments communs à plusieurs classes de tests différentes, par exemple, écrire un test pour une interface et tester les différentes implémentations de ces interfaces. Pour cela, on peut utiliser l'héritage en écrivant les tests dans une classe de test abstraite ayant un attribut du type de l'interface et en écrivant ensuite une classe fille par implémentation à tester, chacune de ces classes filles ayant une méthode @Before différente pour initialiser l'attribut de la classe mère de façon différente.
Vérifier la levée d'une exception
modifierIl peut être intéressant de vérifier que votre code lève bien une exception lorsque c'est pertinent. Vous pouvez préciser l'exception attendue dans l'annotation @Test. Le test passe si une exception de ce type a été levée avant la fin de la méthode. Le test est un échec si l'exception n'a pas été levée. Vous devez donc écrire du code qui doit lever une exception et pas qui peut lever une exception.
@Test(expected = NullPointerException.class)
public void methodCallToNullObject() {
Object o = null;
o.toString();
}
Désactiver un test temporairement
modifierAu cours du processus de développement, vous risquez d'avoir besoin de désactiver temporairement un test. Pour cela, plutôt que de mettre le test en commentaire ou de supprimer l'annotation @Test
, JUnit propose l'annotation @Ignore
. En l'utilisant, vous serez informé qu'un test a été ignoré, vous pouvez en préciser la raison.
@Ignore("ce test n'est pas encore prêt")
@Test
public void test() {
// du code inachevé
}
Écrire de bon tests
modifierIdéalement, les tests unitaires testent une classe et une seule et sont indépendants les uns des autres. En effet, si une classe A est mal codée et qu'elle échoue au test unitaire de A, alors B qui dépend de A (parce qu'elle manipule des instances de A ou hérite de A) va probablement échouer à son test alors qu'elle est bien codée. Grâce à cette propriété on assure, que si un test échoue, c'est bien la classe testée qui est fautive et pas une autre. Nous verrons qu'assurer cette propriété peut être complexe.
Selon les langages, on a souvent des conventions pour nommer les classes de tests. En Java, on choisit souvent d'appeler MaClassTest le test de la classe MaClass. Les tests peuvent être commentés si le déroulement d'un test est complexe.
Évidemment, un bon test assure la qualité de l'intégralité d'une classe, et pas seulement d'une partie de celle-ci. Nous verrons comment vérifier cela dans la partie « qualité des tests ».
Limites du test unitaire
modifierLes classes abstraites
modifierComme l'écriture d'un test unitaire requiert d'instancier une classe pour la tester, les classe abstraites (qui sont par définition non-instantiables) ne peuvent être testées. Remarquons que cela n'empêche pas de tester les classes filles qui héritent de cette classe et qui sont concrètes.
Une solution consiste à créer, dans la classe de test, une classe interne héritant de la classe abstraite la plus simple possible mais qu'on définit comme concrète. Même si cette classe n'est pas représentative des classes filles qui seront finalement écrites, cela permet tout de même de tester du code de la classe abstraite.
Les attributs invisibles
modifierSi un attribut est privé et qu'il ne propose pas d'accesseur public, il n'est pas possible de voir son état sans moyen détourné. Une façon consiste ici à créer une classe interne au test qui hérite de la classe à tester et y ajoute les accesseurs nécessaires. Il suffit alors de tester cette nouvelle classe.
Les méthodes privées
modifierComme vous ne pouvez pas faire appel à une méthode privée, vous ne pouvez vérifier son comportement facilement. Comme vous connaissez l'implémentation de la classe que vous testez (boîte blanche). Vous pouvez ruser et essayer de trouver une méthode publique qui fait appel à la méthode privée.
Une autre méthode, plus brutale et rarement utilisée, consiste à utiliser l'API de réflexion du langage (java.reflect pour Java) pour charger la classe, parcourir ses méthodes et toutes les passer en public.
Les classes ayant des dépendances
modifierUne classe A qui dépend d'une autre classe B pour diverses raisons :
- A hérite de B
- A a un attribut de type B
- une méthode de A attend un paramètre de type B
Du point de vue UML, un trait relie A et B. Or, peut-être que B n'a pas encore été implémentée ou qu'elle n'est pas testée. En plus, il faut veiller à conserver l'indépendance des tests entre eux, et donc, que notre test de A ne dépende pas de la réussite des tests de B.
Pour cela, nous pouvons utiliser des doublures de test : elles permettent, le temps du test de A, de satisfaire les dépendances de A sans faire appel à B. De plus, l'utilisation d'une doublure permet de simuler un comportement de B vis-à-vis de A, qu'il serait peut-être très difficile à simuler avec la véritable classe B (par exemple, des cas d'erreurs rares comme une erreur de lecture de fichier).
À défaut, il faudrait pouvoir indiquer dans les tests qu'on écrit que la réussite du test de A dépend du test de B, parce que si les deux échouent : il ne faudrait pas chercher des erreurs dans A alors qu'elles se trouvent dans B. Ainsi, le framework, prenant en compte les dépendances, pourrait remonter à l'origine du problème sans tromper le développeur.
Des outils complémentaires
modifierNous avons vu quelques limitations que nous pouvons compenser par des astuces de programmation ou l'utilisation de doublures de tests que nous verrons prochainement dans ce livre. Toutefois, il existe des frameworks de tests qui essaient de combler les lacunes de xUnit. Ils méritent qu'on y jette un coup d'œil.
Citons l'exemple de TestNG, pour Java qui permet notamment d'exprimer des dépendances entre tests ou de former des groupes de tests afin de ne pouvoir relancer qu'une partie d'entre eux.
Le logiciel BlueJ représente les classes de tests sous la forme de rectangles verts, exécutables via un clic droit. Elles sont enregistrables à partir d'opérations effectuées à la souris, et héritent toujours de :
public class ClasseTest extends junit.framework.TestCase {
Ses méthodes de test doivent avoir un nom commençant par "test".