I. Introduction

Comme pour chaque article de cette série, les exemples sont en Java, mais le concept vaut pour tous les langages (au moins objets). Dans cet article particulier, je vous décris l'utilisation d'un framework particulier : JUnit. Les concepts restent toutefois les mêmes pour d'autres frameworks, et les bonnes et mauvaises pratiques du chapitre utilisation s'appliquent probablement aussi la plupart du temps. Par ailleurs, pour vous permettre de travailler, je vous donne les URL de téléchargement du framework, pour Java, C++, Delphi et .Net. Pour avoir déjà utilisé le framework de Delphi, je peux vous dire qu'il n'est pas rigoureusement équivalent à celui de Java, mais que l'adaptation du code Java en Pascal est vraiment simple. Les voici donc :

java JUnit
c++ CUnit
Delphi DUnit
.Net NUnit

II. Avant-propos

Le terme framework est régulièrement utilisé dans ce document, je me permets donc d'en proposer une traduction/définition :
Un framework est une infrastructure logicielle qui facilite la conception des applications par l'utilisation de bibliothèques de classes ou de générateurs de programmes, soit dit en quelques mots : un cadre de développement.

III. Concepts

III-A. Quand ?

Quand doit-on écrire des tests, c'est-à-dire à quelle phase du développement doit-on s'y mettre ? Sur ce point, l'une des pratiques les plus en vogue aujourd'hui, l'eXtreme Programming, nous dit de les écrire avant même de commencer à coder. Certains pratiquent aussi le pair programming (programmation en binôme) de la manière suivante : discussion du binôme autour de développement à effectuer (on ne parle pas des cinq jours à venir, mais des 30 minutes à venir) puis chacun retourne à sa machine et l'un des développeurs écrit les tests, tandis que l'autre implémente. Quoi qu'il en soit, les tests s'écrivent en même temps que le code. Légèrement avant, légèrement après ou exactement en même temps ? Peu importe votre méthode, mais vous ne devez pas écrire tous les tests d'un coup avant le développement, ni les écrire après avoir terminé l'implémentation (ce serait totalement inutile). Pour ma part, j'opte plutôt pour l'écriture des tests avant le code ; en effet, cette pratique a plusieurs avantages :

  • affiner l'analyse, en particulier, si en écrivant le Javadoc avant le code, vous avez recensé les besoins, ici vous recensez les cas d'utilisations ;
  • apporter l'ensemble des règles pré et postconditions traitées dans l'article sur les assertions ;
  • apporter aussi l'ensemble des traitements de vérification de paramètres également traités dans un article précédent ;
  • éviter d'écrire du code inutile. En effet, si vos tests (bien écris s'entend) passent tous, inutile d'ajouter du code à votre classe. Donc vous implémentez jusqu'à ce que tous les tests passent, puis vous arrêtez et passez au point suivant.

III-B. Documenter !

Ne vous méprenez pas sur ce titre, il ne s'agit pas de documenter le test, mais de s'en servir comme d'une documentation technique. En effet, l'utilisation de vos classes est toujours difficile à documenter. Qui plus est, une documentation à part risque fort d'être rapidement désuète. Ceci vous décourage dans l'écriture d'une documentation et vous mène souvent à la rédaction de celle-ci en fin de projet, soit beaucoup trop tard. Le test est en lui-même un exemple d'utilisation et de manipulation de vos classes, il vous permet donc de documenter celles-ci. Donc, en plus d'ajouter de la valeur logicielle, le test apporte de la valeur documentaire à vos codes, vous hésitez encore à en écrire ?

III-C. Une simulation d'IHM développée en quelques minutes

Si vous partez d'un code qui fonctionne, que vous lui ajoutez un peu de fonctionnel, où se trouve le bogue ? Dans les lignes que vous avez ajoutées bien entendu ! Donc plus vous testez régulièrement, plus le bogue est facilement détectable. Eh oui, dénicher un bogue dans deux ou trois lignes de code regroupées est beaucoup plus simple que de le détecter dans une trentaine de lignes réparties sur plusieurs classes ! Alors le problème c'est qu'aujourd'hui, vous êtes obligé d'écrire, le code métier, le code de persistance et le code de l'IHM avant de pouvoir vraiment tester quoi que ce soit. Le framework de test vous apporte donc une solution sur mesure pour résoudre ce problème : il faut quelques lignes seulement pour écrire le client graphique (même s'il ne se présente pas franchement comme l'IHM définitive). Vous vous affranchissez donc de deux problèmes en une seule fois : ne pas être obligé de coder l'interface graphique tant que le métier n'est pas implémenté, et ne pas dépendre du débogage de l'IHM elle-même pendant le codage du métier. Toujours des hésitations ?

III-D. Quelques secondes pour mettre toute votre application à l'épreuve

Une fois les tests écrits, il suffit de quelques secondes pour les lancer, et obtenir un compte-rendu. Et vous obtenez en quelques secondes, ou quelques minutes si votre application est vraiment importante et que vous lancez vraiment absolument tous les tests, un compte-rendu exhaustif de ce qui marche et de ce qui ne marche pas ! Avez-vous déjà pris le temps de tester une application de manière exhaustive ? C'est pourtant ce qu'il faut faire avant de la livrer, et ce qu'il faudrait faire à chaque modification. Autant dire que même pour une application de petite taille, si vous la testez manuellement, ça vous prendra au bas mot plusieurs dizaines de minutes ! Vous ne le faites donc que rarement et ça vous est préjudiciable, vous allez encore passer des heures à déboguer un petit rien simplement parce qu'au moment où vous avez généré le bogue, vous n'avez pas tout testé ! Si vous n'êtes toujours pas convaincu par le fait qu'il faut écrire des tests, j'ai encore un paragraphe pour vous faire changer d'avis, mais vous êtes un peu dur ;-)

III-E. Non-régression

Combien de fois ai-je entendu "Je ne comprends pas pourquoi, hier ça marchait ! " ? Impossible de répondre… Trop souvent en tout cas. Le pire, étant certainement de l'avoir entendu sortir de sa propre bouche… Vous ne voulez plus vous entendre dire à votre responsable qu'hier ça marchait, ou vous ne voulez plus entendre vos collègues vous dire ce genre de lieu commun des développeurs. De plus, afin que la maintenance de votre code puisse être effectuée par tous, sans pour autant que de nouveaux bogues soient introduits par un tiers, il faut lever un drapeau "attention, ne marche pas comme prévu ! ". Les tests unitaires sont là pour ça. La non-régression du code, c'est donc, la certitude que l'on avance. C'est aussi la confiance qui s'installe : "je peux modifier mon code puisqu'il résiste au pire, je saurais immédiatement s'il y a bogue, où et pourquoi". Et la confiance en soi donne la force d'avancer plus vite. Alors, perdre son temps à écrire quelques tests, c'est gagner des heures de débogage (ce qui vous intéresse le moins dans votre métier j'en suis certain), ça n'est donc pas utile, mais essentiel. Dans quelques semaines, après avoir acquis suffisamment de réflexes pour l'écriture de ces tests, vous repenserez à cette période ancestrale durant laquelle vous écriviez du code sans écrire de tests, et vous vous demanderez alors comment vous faisiez.

IV. Utilisation

IV-A. Écrire une série de tests pour une classe

Commençons par observer la syntaxe à respecter pour écrire un test, ensuite, nous verrons comment réunir plusieurs séries de tests pour un lancement simultané. Enfin, une fois que l'on sera capable d'écrire les tests, on essayera de débrouiller les situations pour lesquelles écrire les tests et dans quelles conditions les écrire. Une classe héritant de junit.framework.TestCase pour commencer, c'est le socle de votre série. Dans cette classe, vous pouvez définir les séries de tests tout simplement en déclarant des méthodes publiques dont le nom commence par les quatre lettres (en minuscules) test. Voilà, vous avez défini un testCase que vous pouvez passer en paramètre à un TestRunner pour lancer vos tests. C'est simple non ? Bien entendu, il reste encore à comprendre ce qu'il faut mettre dans les méthodes testXXX(). Vous n'allez pas être dépaysé si vous avez déjà lu l'article sur les assertions, car les tests se font justement à coup d'assertions. Voilà un exemple de test, je mets une méthode main()dans la classe afin de la rendre exécutable, il n'y a rien d'obligatoire à ça bien entendu.

 
Sélectionnez
package com.developpez.tutoriels.astuces.tests;
 2
 3  /**
 4   * classe de test pour une pseudo classe de traitement d'une facture
 5   */
 6
 7  public class TestFacture extends junit.framework.TestCase {
 8          public void main(String[] args) {
 9                  // pas de vérification des paramètres, ce n'est pas l'objet
10                  junit.textui.TestRunner.run(TestFacture.class);
11          }
12
13          public TestFacture(String name) {
14                  // ce constructeur est obligatoire, car il n'existe pas 
15                  // de constructeur par défaut dans les TestCase
16                  super(name);
17          }
18
19          public void testAjoutArticles() {
20                  Facture maFacture = new Facture();
21                  maFacture.add(new Article("article 1", 3, 150.0));
22                  maFacture.add(new Article("article 2", 1, 50.0));
23
24                  assertNotNull("La facture ne devrait pas être null", maFacture);
25                  assertEquals("Le total de la facture est mal calculé",
26                                  3 * 150 + 50, 0.0001,
27                                  maFacture.getTotal());
28                  assertEquals("Le nombre d'articles est mal calculé",
29                                  4,
30                                  maFacture.countArticles());
31          }
32          public void testValidationFacture() {
33                  Facture maFacture = new Facture();
34                  maFacture.add(new Article("article 1", 3, 150.0));
35                  maFacture.add(new Article("article 2", 1, 50.0));
36                  maFacture.valide();
37
38                  assertTrue("la validation de la facture n'a pas eu lieu",
39                                  maFacture.isValide());
40
41                  try {
42                          maFacture.add(new Article("article 3", 1, 20.20));
43                         fail("facture modifiée après validation");
44                  } catch (FactureException e) {
45                          // interdit de modifier une facture valide donc 
46                          // c'est normal d'être ici
47                          
48                  }
49          }
50  }

À remarquer tout de suite dans ce code :

  • j'utilise le testRunner en mode texte, je le trouve plus pratique, car il envoie tout sur la sortie standard ou erreur et en général votre IDE permet de vous diriger directement vers le code incriminé en cas d'erreur. Il existe toutefois un testRunner AWT et un SWING qui présente les résultats de manière plus graphique. À vous de choisir ;
  • je vous indique quelques méthodes assertXXX, il en existe beaucoup, leur nom est suffisamment parlant pour que je ne vous fasse pas un glossaire exhaustif ;
  • l'assertion sur l'égalité de réels se fait à l'aide de trois réels, les deux réels à comparer, et une approximation ;
  • les assertions d'égalités vous livrent comme message "value" expected but "value" found ;
  • un message (String) peut être spécifié pour toutes les assertions, il sera livré si elles ne sont pas vérifiées ;
  • enfin, le nom des méthodes testXXXX sera repris dans les messages, veillez à ce qu'il signifie quelque chose.

IV-B. Réunir les tests en une suite de tests

Comme lancer un test pour une classe est loin de tester l'application complète, il est possible de réunir les tests dans des faisceaux de tests, les TestSuite. Ensuite, leur lancement se fait de la même manière qu'avec un TestCase. Bien entendu, vous pouvez réunir plusieurs TestSuite dans un TestSuite plus important, etc. jusqu'à obtenir un test de l'application complète. Ceci est avantageux sur une application pour laquelle les tests peuvent prendre plusieurs minutes, en effet, l'intérêt des tests est de pouvoir les lancer presque toutes les cinq minutes afin de détecter très tôt les bogues.

IV-C. Bonnes pratiques

IV-C-1. Une classe, un test

Une classe devant être testée, autant qu'elle dispose de son propre module de tests. Comme la notion de module de granularité la plus fine en Java est la classe, on écrit une classe de test pour chaque classe à tester. Cela vous permet de retrouver facilement le test attaché à une classe ; pour ce faire, il suffit de prendre la convention de nommage : TestMaClass et le tour est joué.

IV-C-2. Tests dans le même package que la classe à tester

Cette résolution est simple à expliquer. Si vous voulez pouvoir tester l'intérieur d'un composant (classe déclarée de visibilité niveau package ou méthodes déclarées de visibilité niveau package), il vous faut écrire la classe de tests dans le même package. Par ailleurs, pour retrouver la classe de tests spécifique d'une classe, il est plus facile de la chercher dans le même package. Enfin, si deux classes portent le même nom, mais se trouvent dans des packages différents, autant que les classes de tests se trouvent aussi dans des packages différents.

IV-C-3. Scénarios

IV-C-3-a. Écrire les scénarios courants

Vous avez maintenant écrit les assertions pour vous assurer de la véracité de ce que vous supposiez avoir écrit. À présent, il vous reste à écrire les scénarios d'utilisation courante de votre classe. En effet, c'est déjà important que ce qui est attendu fonctionne, mais les méthodes s'appellent généralement suivant des enchaînements logiques, qui doivent aboutir à une réalisation donnée, autant s'en assurer.

IV-C-3-b. Et surtout mettre l'application en difficulté

Écrire les scénarios catastrophes pour vérifier que l'application résiste bien. Que l'utilisateur à une bonne chance de recevoir un message clair dans ce type de cas. Bref, faire faire par vos TestCase le test du client qui s'assied sur le clavier.

IV-D. Mauvaises pratiques

IV-D-1. Ne pas mettre les classes dans le même répertoire que les tests

En effet, évitez de mettre ensemble les sources des tests avec celles de l'application, car il est plutôt probable que vous n'ayez pas envie, en particulier parce que ce n'est pas judicieux, de livrer et déployer votre application avec les tests. Pour pouvoir gérer aisément la fabrication des jars et la compilation, il est donc préférable de séparer les sources.

IV-D-2. Ne pas écrire de tests triviaux

Écrire des tests triviaux revient à perdre du temps : c'est inutile. Ça donne l'impression que l'écriture de tests fait perdre du temps : c'est fallacieux. Malheureusement il est plus facile de dire n'écrivez pas de tests triviaux que de déterminer la trivialité ou non d'un test. Comme précisé dans la rubrique précédente, pour être certain d'avoir écrit des tests, si possible non triviaux, le mieux est de commencer par écrire les scénarios attendus, et les scénarios catastrophes. Vous obtenez déjà un bon point de départ. Ensuite, ajoutez tous les invariants de classe si vous n'utilisez pas déjà un framework de programmation par contrat comme expliqué dans l'article sur l'utilisation des assertions.

IV-D-3. Pas d'effet de bord dans les tests

Vos tests doivent pouvoir tourner 100, 200, 1000 fois et plus encore… Si l'un de vos tests provoque des effets de bord sur l'état de votre système, vous aurez alors des tests qui ne se vérifient qu'une seule et unique fois. Vous devez donc absolument éviter les effets de bord dans vos tests.

IV-D-3-a. Ne présumez pas de l'ordre dans lequel les tests sont lancés

En particulier, ne présumez pas de l'ordre dans lequel seront lancés vos tests. En effet, le framework utilise l'introspection (java.lang.reflect), mais vous n'avez aucune idée de la manière dont la JVM implémente cette introspection, est-ce qu'elle prend les méthodes dans l'ordre d'implémentation ? Dans le sens inverse à cause d'une pile (j'empile, je désempile) ? ou par ordre alphabétique (pourquoi pas ?), etc. Vous ne pouvez donc pas écrire des tests du type testInsert testModif testSupprime ou test insert insert l'enregistrement que testModif modifie et que testSupprime supprime. Il faut donc tester un scénario complet, de la création à la suppression en passant par la modification.

IV-D-3-b. Ne laissez pas, surtout dans les couches de persistances, d'effets de bord

Pensez aussi à laisser le système précisément dans l'état dans lequel il se trouvait avant que les tests ne soient lancés, surtout pour toute la partie persistante. En effet, imaginez que vous écriviez un test d'insertion de société dans votre système, mais que vous ne pensiez pas à supprimer cette société de la base de données sur laquelle votre système repose. Que se passera-t-il au deuxième lancement de vos tests, sachant que l'une des contraintes sur les sociétés est l'unicité de raison sociale ? Eh oui, le test d'insertion échoue alors que tout se passe à merveille puisque même la contrainte est traitée. Donc le test ne fonctionne qu'une fois, autant dire que c'est inutile !

V. Conclusion

En résumé, écrire des tests permet

  • une analyse de petite granularité ;
  • la certitude d'engendrer peu de bogues ;
  • la non-régression du code ;
  • la documentation efficace de votre code.

L'écriture des tests n'est pas difficile et n'est pas longue non plus (à condition d'être pratiquée au fur et à mesure du développement). Pour comble, apprendre à coder son premier test ne prend que dix minutes. Et même si pour apprendre à écrire efficacement ses tests, il faut de la pratique, en écrire "peu efficacement" reste plus efficace que de ne pas en écrire du tout. J'ai beau essayer d'explorer ce qui pourrait nous pousser à ne pas en écrire, je ne trouve rien de probant. Il ne reste donc qu'une conclusion possible : dès demain… tut tut tut… non dès tout de suite un clic sur le lien qui correspond à votre environnement de travail pour un download immédiat.

Modifications entre la version 1.0 et 1.1 : corrections de bogues dans le code exemple en Java.
  • Ligne 25 et 28 l'assertEquals() s'écrit assertEquals(Message en cas d'échec, valeur attendue, valeur trouvée)
  • Ligne 38 assertXXX s'écrit toujours avec le message comme premier paramètre si un message est fourni.
  • Ligne 48 déplacée en 43 et suppression du return pour permettre de lancer d'autres tests à la suite de celui-ci.