:: Enseignements :: Master :: M1 :: 2012-2013 :: Java Avancé ::
[LOGO]

JUnit en quelques mots


Document inspiré de la présentation de Jérôme Cheynet

Introduction

Il existe un JUnit Cookbook. Malheureusement, le Cookbook n'est pas un bon point de départ quand on ne connait pas JUnit, car toutes les techniques pour créer un test sont abordées, alors qu'en fait la plupart des gens utilisent une seule de ces techniques. Après avoir lu ce qui suit, ou tout autre exemple simple, vous pourrez approfondir vos connaissance sur JUnit avec ce CookBook.

Nous allons dans ce document présenter l'écriture de tests unitaires simples pour une classe simple : une calculatrice à 4 opérations. Nous allons aborder la méthode de fabrication et d'invocation d'un test qui est la plus souvent utilisée. Nous verrons également comment utiliser les tests unitaires avec Ant.

Un exemple simple de test

Dans la suite de ce document nous développerons des tests pour la classe calculator suivante:

La technique pour créer un test consiste à créer une nouvelle classe, portant pour des raisons de commodité le nom de la classe que vous voulez tester, suivi de "Test". Cette nouvelle classe doit hériter de la classe "TestCase".

Avec Eclipse, vous ne pourrez hériter de TestCase qu'en ayant ajouté le jar de JUnit à votre build path.

Vous bénéficiez d'un Wizard pour créer le test unitaire (cliquez au préalable sur la classe à tester).

Nous réalisons maintenant un "test de bon fonctionnement" d'une des méthodes de la classe testée. Toujours pour des raisons de commodité, il est bon de reprendre le nom de la méthode testée. Cependant il arrive quelques fois qu'une méthode ait plusieurs tests qui lui soient associés, dans ce cas, vous devrez trouver des noms permettant de comprendre tout de suite d'où vient le problème (si le test échoue, c'est ce nom qui vous sera présenté, plus éventuellement des informations supplémentaires comme nous allons le voir).

Points très importants :
  • la méthode doit avoir un nom débutant par "test";
  • elle doit être déclarée public, et ne rien renvoyer (void).

Voici quelques exemples d'une telle méthode :

Si un test échoue, e.g. si les deux arguments de assertEquals sont différents, cela permet d'avoir un message d'erreur qui a un sens (expected ... but was ...) :

junit.framework.AssertionFailedError: expected:<8> but was:<7>

at junit.framework.Assert.fail(Assert.java:47)

at junit.framework.Assert.failNotEquals(Assert.java:282)

at junit.framework.Assert.assertEquals(Assert.java:64)

at junit.framework.Assert.assertEquals(Assert.java:201)

at junit.framework.Assert.assertEquals(Assert.java:207)

at fr.umlv.exposeJUnit.calculators.FourOpCalculatorTest.testAdd(FourOpCalculatorTest.java:45)
Remarque : Si nous n'avions pas fait d'appel à la méthode assert, le test passerait à tous les coups. JUnit ne vous oblige pas à faire un test qui passe par une de ces méthodes de validation, il ne vous oblige aucunement à écrire un test significatif. L'existence d'un test unitaire n'apporte a priori pas de garantie particulière sur la qualité du code testé.

Lancement du test unitaire

Un test ne se lance pas comme un programme java normal. Avec l'IDE Eclipse, la vue graphique s'obtient facilement. Il suffit de lancer le TestCase en temps que "JUnit Test" (Run As... JUnit Test). Par défaut, vous verrez une barre rouge en cas de problème, et seulement un message dans la barre d'état en cas de réussite. Il est possible de voir aussi la barre verte en décochant une option.

Ecrire un test plus complexe

Nous avons vu comment écrire un test simple pour une classe également simple. Le framework JUnit met à disposition d'autres outils qui permettent de faciliter l'écriture des tests unitaires : la classe Assert. Le propre d'un test unitaire est d'échouer quand le code testé ne fait pas ce qui est prévu. Pour faire échouer un test JUnit (c'est à dire une des méthodes testXXX> d'un TestCase), il faut utiliser une des méthodes de la classe junit.framework.Assert, qui sont toutes accessibles au sein d'un TestCase.

La méthode assertEquals permet de tester si deux types primitifs sont égaux (boolean, byte, char, double, float, int, long, short). L'égalité de deux objets peut être testée également (attention, ce n'est pas un test sur la référence). Pour les double et les float, il est possible de spécifier un delta, pour lequel le test d'égalité passera quand même.

Les méthodes assertFalse et assertTrue permettent de tester une condition booléenne.

Les méthodes assertNotNull et assertNull permettent de tester si une référence est non nulle.

Les méthodes assertNotSame et assertSame permettent de tester si deux objets ont la même référence.

La méthode fail fait échouer le test sans condition. En cas d'utilisation de fail, il est encore plus conseillé que pour les autres méthodes de faire figurer un message expliquant pourquoi le test a échoué.

Les messages d'erreur peuvent être personnalisés. Les méthodes de test existent toutes sous deux formes : l'une qui ne prend pas de message, et l'autre qui en prend un en tant que premier paramètre. On obtient un message de ce type :
junit.framework.AssertionFailedError: add marche mal expected:<8> but was:<7>
  at junit.framework.Assert.fail(Assert.java:47)
  at junit.framework.Assert.failNotEquals(Assert.java:282)
  at junit.framework.Assert.assertEquals(Assert.java:64)
  at junit.framework.Assert.assertEquals(Assert.java:201)

  at fr.umlv.exposeJUnit.calculators.FourOpCalculatorTest.testAdd(FourOpCalculatorTest.java:45)
 

Nous avons vu que les méthodes de test étaient annotées par @Test. Les méthodes annotées par @Begin (resp. @End) sont appellées avant (resp. après) chaque appel aux méthodes de tests. Par exemple, pour :
les méthodes seront appellés dans l'ordre suivant :
createOutputFile()
testSomethingWithFile()
deleteOutputFile()
createOutputFile()
testSomethingElseWithFile()
deleteOutputFile()
			  

Compilation et tests unitaires dans une même opération avec les TestSuites et Ant

Le test permet de savoir si le code fait bien ce qu'on lui demande. La compilation permet de savoir si la syntaxe du code est correcte. Pourquoi ne pas compiler et tester dans un même opération ? C'est ce qu'il est possible de faire avec Ant et un TestSuite qui va nous permettre d'agréger tous les tests créés.

Réaliser un groupement de tests est simple. Il suffit pour cela de créer une classe TestSuite définissant une méthode publique, nommée suite, renvoyant un objet de type Test qui peut contenir un TestCase ou un TestSuite. Attention, pour réaliser vos TestSuite, vous devez reprendre le même prototype de fonction. A noter que là encore, Eclipse propose un Wizard automatisant la création du TestSuite.

Voici un script Ant pour la pseudo-application de calculatrice. La dernière tache runtests lance la classe JUnit.textui.TestRunner avec java et avec pour paramètre le TestSuite AllTests, qui rassemble tous les tests unitaires du projet.

Pour aller plus loin

Maintenant que vous savez ce qu'est JUnit, comment écrire un test, une suite de test, et utiliser le tout avec Ant, vous pouvez songer à améliorer votre connaissance des test unitaires.

"If the programmer writes bad code to begin with, how can you expect anything of better quality in the tests?", Marc Clifton. Nous venons de voir des tests simples, où l'on attend un résultat spécifique du code testé. Réaliser ce genre de tests peut être très utile et suffisant dans la plupart des cas. Néanmoins certaines personnes ne se satisfont pas d'un seul test de ce genre par méthode. En effet, cela ne permet de dire qu'une chose, c'est que le code fait ce qu'on attend de lui lorsqu'on lui passe les mêmes paramètres que ceux du test.

Si vous testez une des méthodes d'une calculatrice, qui calcule la puissance au carré d'un nombre, et que vous faites le test pour 2^2, vous faites un test qui attend le nombre 4. Mais un tel test signifie à la fois peu et beaucoup : beaucoup, parce que la méthode semble faire ce qu'on lui demande. Et peu, parce que l'addition et la multiplication auraient renvoyé le même résultat. C'est pourquoi faire des tests unitaires ne se limite pas forcément à tester une expression pour deux raisons. La première est qu'un code peut produire l'expression attendue par "hasard". La seconde est qu'en testant une expression, on ne teste qu'un seul "chemin".

Prenons l'exemple d'une classe permettant de copier un fichier en local et en distant. Si vous testez à un niveau trop haut, c'est à dire que vous ne testez pas toutes les méthodes privées qui constituent cette classe, vous allez devoir tester deux fonctionnalités : la copie en local, et la copie en distant, qui constituent deux "chemins". Si vous perdez de vue les 2 chemins possibles, vous n'allez tester que la copie en local, ou bien, que la copie en distant. C'est pourquoi il faut écrire un test par "chemin". Vous trouverez des choses intéressantes sur les améliorations possibles des outils de tests unitaires, applicables également à vos propres tests, sur la page Advanced Unit Test, Part V - Unit Test Patterns de Marc Clifton. La première partie (Part I) de l'article est aussi très intéressante : elles permet entre autre de se convaincre de l'utilité des tests unitaires, suivant des exemples plus classiques (sans le "test driven developpement" de l'Extreme Programming que les auteurs de JUnit expliquent en même temps que les tests unitaires).