Quand vous demandez à une équipe de développement si elle réalise des tests unitaires, elle dira toujours oui. Quand vous creusez, la signification du terme « test unitaire » est très variable : test manuel spécifié par un document, test automatisé d’IHM, classe de test avec un main ()… Finalement peu de projets industrialisent leurs tests unitaires avec des frameworks dédiés.
Tout d’abord parce que, aussi étonnant que cela puisse paraître, tout le monde ne connaît pas l’existence de ces frameworks, ou alors ne mesure pas bien leur intérêt par rapport à de simples main(). Pour rappel, un test unitaire, tel qu’on l’entend généralement en développement, est un test automatisé qui cible une méthode particulière d’une classe. Le niveau de granularité est donc le plus fin possible. Des frameworks spécialisés sur les tests unitaires existent et offrent les avantages suivants :
- ils formalisent les tests selon une organisation et une écriture cohérentes ;
- ils automatisent leur exécution ;
- ils produisent des rapports d’exécution pour identifier les tests en échec ou les tests trop longs.
Un des avantages du monde .NET par rapport au monde Java est que l’environnement phare de développement Visual Studio propose par défaut un framework de tests unitaires (MSTest). Il n’est donc pas obligatoire d’aller chercher un framework tiers pour l’intégrer dans son développement comme en Java. En Java, deux frameworks Open Source monopolisent le marché : JUnit et TestNG. Côté .NET, il existe des alternatives Open Source à MSTest, comme NUnit et MbUnit. Globalement, tous ces frameworks se valent pour une utilisation basique.
Comment commencer ?
Une fois qu’on a choisi son framework de tests unitaires, le problème est de savoir par où commencer, quelles méthodes tester en priorité. Plusieurs stratégies sont possibles :
- tester les méthodes les plus faciles à tester ;
- tester les méthodes les plus utilisées (méthodes de couches utilitaires) ;
- tester les méthodes les plus risquées du point de vue technique (code complexe) ;
- tester les méthodes les plus risquées du point de vue métier (fonctionnalité critique pour les utilisateurs) ;
- ne tester que le nouveau code, mais de manière systématique.
La bonne stratégie est probablement un mélange de ces différentes options. Un point important à noter est que le code existant sera généralement plus difficile à tester, parce que non conçu avec l’objectif de le tester unitairement. En effet, quand vous êtes déjà habitués à implémenter des tests unitaires, vous développez différemment, avec l’optique que votre code soit facilement testable : découpage plus fin des méthodes, minimisation des dépendances (par exemple en recourant à des frameworks d’injection de dépendances), simplification des paramètres… C’est d’ailleurs un des grands intérêts de l’écriture de tests unitaires.
Faut-il tout tester ?
Les experts vous diront qu’un code non testé est un code jetable. L’idée est qu’on ne peut pas modifier un code non testé sans être sûr de ne pas introduire de régression. Cette affirmation est à nuancer pour les raisons suivantes :
- tester peut coûter cher. Les extrémistes des tests unitaires soutiendront que tester à 100% son code ne revient pas plus cher eu égard au temps gagné sur le traitement des bugs. En tenant compte du temps passé à mettre à jour les tests unitaires suite aux évolutions du code (ce qui coûte généralement plus cher que l’initialisation des tests), cette affirmation est assez contestable ;
- une méthode testée n’est pas forcément bien testée. Il ne suffit pas de créer un test pour garantir que le traitement est valide. Le test lui-même peut comporter des erreurs ou être incomplet ;
- quelle est la valeur des tests de méthodes formelles telles que les accesseurs des classes (getter/setter, propriétés) ?
- le code Java/C#/PHP n’est généralement qu’une partie de l’application. En suivant le principe de tout tester, il faut donc également tester unitairement l’IHM, la base de données, l’annuaire LDAP… Les frameworks de tests unitaires existent désormais pour de nombreux environnements, mais le coût de mise en œuvre augmente considérablement.
J’ai écrit mes premiers tests unitaires en 2000 avec JUnit, avec l’objectif de tester mon code à 100 %, dans le cadre d’un développement au sein d’un éditeur. J’ai tenu 3 mois avant de revenir à un objectif plus mesuré.
Il est donc préférable de tester intelligemment. Tester intelligemment consiste à cibler les traitements les plus risqués et sur lesquels l’effort de test est réaliste. Ceci est similaire à la démarche pragmatique que l’on peut avoir sur l’analyse qualité : est-il vraiment utile de s’imposer des objectifs hyper exigeants si c’est pour exploser le délai de livraison ? Toutes les applications ont des défauts, techniques ou fonctionnels. Le point-clé est de bien les identifier et de mettre en balance l’intérêt de les corriger avec le coût de correction pour traiter en priorité les points critiques, et surtout de s’assurer de ne pas reproduire ces défauts dans les nouveaux développements.
Mesurer intelligemment la couverture des tests
Il est intéressant de connaître le niveau de test d’une application pour mesurer l’effort accompli et comparer ce niveau avec des versions précédentes ou avec d’autres applications similaires. Un premier indicateur est le nombre de tests unitaires. C’est un bon indicateur, simple, mais qui ne traduit pas l’étendue des tests au sein de l’application : de nombreux tests peuvent concerner uniquement une petite partie de l’application.
Le taux de couverture de code est un indicateur plus complet. Il mesure la proportion du code exécuté par les tests unitaires. La mesure peut porter sur le nombre de lignes de code exécutées, sur le nombre de conditions, de méthodes… On obtient dans tous les cas un pourcentage (par exemple 66 %) de l’application couverte par les tests unitaires. La couverture de code est mesurée par des outils spécialisés. Les plus connus côté Java sont Cobertura, EMMA, JaCoCo et Clover (le seul non Open Source des 4, par Atlassian). Côté .NET, on trouve NCover, Partcover, dotCover et aussi la fonction native de Visual Studio Team System.
Les outils de couverture sont très peu utilisés, même pour des projets qui réalisent des tests unitaires. Pourtant leur mise en œuvre est assez abordable (surtout quand ils ne demandent pas de retraiter le code avant déploiement, par exemple JaCoCo s’exécute avec une simple option de la JVM (javaagent:jacocoagent.jar), mais l’interprétation et l’exploitation de leurs résultats sont délicates.
Le taux de couverture de code peut être utilisé comme objectif de test, certains s’imposant des taux de 90 % voire 100 %. Comme souvent, ce sont les derniers pourcentages les plus fastidieux à atteindre et on peut douter de l’efficacité de ce type d’objectif au niveau de la qualité des tests.
Il est également possible de croiser ce taux brut de couverture avec la pertinence à tester les traitements pour proposer des objectifs plus réalistes. Une méthode qui présente un fort intérêt à être testée se verra associer un objectif de couverture fort (exemple : 90 %), alors qu’une méthode pour laquelle un test n’aurait aucun intérêt aura un objectif de couverture de 0 %. De manière globale, nous pouvons ainsi pondérer le taux de couverture de l’application en tenant compte de ces différents objectifs, avec comme résultat possible qu’une application affiche un taux de couverture pondéré de 80 % alors que sa couverture brute n’est que de 50 %. De cette manière, l’équipe projet peut se focaliser sur les tests les plus efficaces.
Conclusion
Nous sommes toujours surpris de l’écart entre les pratiques recommandées et les pratiques mises en œuvre. On peut toujours parler de TDD (Test Driven Development) ou de couverture de code, mais l’effet est limité quand le projet n’est toujours pas passé aux tests unitaires ou alors que les tests ont été abandonnés faute d’intérêt. Il est donc important de rendre pragmatiques des notions qui paraissent trop souvent théoriques et trop détachées du terrain pour la majorité des développeurs.
Par Sylvain François, Directeur R&D chez Kalistick