Développons en Java 2.30 | |
Copyright (C) 1999-2022 Jean-Michel DOUDOUX | (date de publication : 15/06/2022) |
|
Niveau : | Elémentaire |
Le but d'un test est de vérifier qu'une fonctionnalité fait ce que l'on attend d'elle.
Les tests d'une application sont une phase très importante dans les cycles de développement et de maintenance d'une application. Ils permettent de détecter des bugs et de s'assurer que l'application réponde au cahier des charges et aux spécifications.
Ces tests peuvent prendre différentes formes :
La mise en oeuvre des tests peut être facilitée par l'utilisation d'outils :
Ce chapitre va essentiellement se concentrer sur la mise en oeuvre de quelques-uns de ces tests avec certains outils.
Ce chapitre contient plusieurs sections :
Les tests unitaires peuvent être réalisés de différentes manières :
L'utilisation d'un débogueur peut être pratique pour tester du code fraîchement écrit et comprendre son fonctionnement mais il ne permet pas d'automatiser ces tests. En effet, cette technique requiert une intervention manuelle et une interprétation humaine des résultats.
C'est la même problématique avec l'utilisation de traces avec des System.out ou l'écriture dans un fichier : les données de ces traces doivent être analysées par une personne.
L'utilisation de frameworks dédiés à l'automatisation des tests unitaires permet d'assurer une meilleure qualité et fiabilité du code. Cette automatisation facilite aussi le passage de tests de non-régression notamment lors des mises à jour du code. De plus, l'utilisation de ces frameworks ne nécessite aucune modification dans le code à tester ce qui sépare clairement les traitements représentés dans le code de leurs tests. Enfin, l'analyse des résultats peut être automatisée puisque chaque résultat de tests possède un statut généralement ok ou en erreur.
Ces frameworks ne sont que des outils qui permettent la mise en oeuvre de tests unitaires mais ils ne dispensent pas d'utiliser une méthodologie pour mettre en oeuvre ces tests.
Un test unitaire se déroule en quatre étapes :
Les tests unitaires automatisés sont très importants et ce durant tout le cycle de vie d'une application :
L'utilité des tests unitaires automatisés n'est plus à démontrer : ils sont même primordiaux dans certaines méthodologies notamment XP (eXtreme Programming) et TDD (Test Driven Development). Ils servent à promouvoir et vérifier la qualité et la fiabilité du code.
Les tests unitaires automatisés sont un des outils les plus puissants pour améliorer la qualité d'une application. De plus, l'utilisation de tests unitaires améliore l'organisation et la stabilité du code.
Les tests unitaires n'ont pas qu'un effet de test immédiat du code mais surtout ils permettent d'effectuer des tests de non-régression lors de modifications qui interviennent inévitablement durant la vie d'une application. Les tests unitaires automatisés sont donc particulièrement intéressants pour les tests de non-régression qui seront automatisés. Il est courant d'avoir des portions de code fréquemment perçues comme mystiques car personne ne comprend plus comment elles fonctionnent malgré le fait que ce code soit primordial. Il est alors toujours délicat de faire évoluer ce code lors de maintenances correctrices ou évolutives.
La présence de tests unitaires automatisés va rassurer le développeur car il pourra réexécuter ces tests avant et après les modifications pour s'assurer qu'il n'y a pas de regression. Bien sûr, le dégré d'assurance augmente avec la croissance du nombre de tests et leur qualité.
L'écriture de cas de tests permet de prouver que le code à tester fonctionne. Les cas de tests permettent ensuite de s'assurer de la non-régression lors des maintenances dans le code. Les tests unitaires permettent de capitaliser sur les tests à effectuer et ainsi de limiter les effets de bord liés aux inévitables modifications correctrices ou évolutives du code.
La POO implique naturellement des dépendances entre les classes. Une modification dans une de ces classes peut facilement induire des effets de bord dans les classes appelantes. Si les tests sont complets et corrects, une modification ayant un effet de bord fera échouer les tests existants. Dans ce cas, soit la modification nécessite une adaptation du cas de tests soit un bug a introduit un effet de bord dans le comportement du code.
L'existence de tests unitaires couvrant une majorité des cas de tests permet d'être plus confiant lors de la modification de code : cela peut améliorer la garantie qu'une modification n'a pas d'effet de bord.
Si une classe possède un ensemble complet de tests unitaires, il y a moins de réticences à faire des modifications dans son code lors des maintenances correctives ou évolutives, pour améliorer les performances ou pour faire du refactoring. Les tests permettent de s'assurer de la non-régression des fonctionnalités proposées.
Au fur et à mesure que des modifications sont faites dans le temps, les risques augmentent dans une application qui ne possède pas ou peu de tests unitaires. L'absence de tests automatisés implique des tests manuels qui peuvent être oubliés ou mal interprétés augmentant ainsi le risque de ne pas détecter d'effets de bord.
Le coût d'écriture des tests est largement compensé par celui gagné par la réutilisation des tests à chaque itération corrective.
Il est plus facile d'effectuer des opérations de refactoring si les classes disposent d'un ensemble des tests unitaires complets.
Les tests unitaires sont les premiers tests réalisés parmi l'ensemble des tests qui seront réalisés sur l'application. Il ne faut surtout pas les sous-estimer en se disant que les tests suivants permettront de détecter les bugs car leur grand avantage est qu'avec un framework dédié ils peuvent être automatisés.
La rédaction de tests unitaires implique nécessairement une amélioration de la conception du code. Il est très facile d'écrire du code lorsque celui-ci ne doit pas être testé. Cependant pour écrire du code qui doit être testé, il faut que la conception du code soit adaptée pour faciliter la mise en oeuvre des tests unitaires :
Les tests unitaires peuvent facilement servir d'exemples d'utilisation du code testé puisque le code est nécessairement invoqué durant les tests.
Il est encore fréquent de voir des scénarios de tests écrits dans un document et exécutés manuellement par un humain. Cette approche est obsolète dans la mesure où des outils existent pour automatiser une bonne partie de ces tests évitant ainsi les erreurs humaines (aucune exécution des tests, oublie de l'exécution de cas, mauvaise interprétations des résultats, ...). De plus, les fonctionnalités d'une application ont tendance à augmenter avec le temps ce qui rend ce processus encore plus long et fastidieux.
Il existe plusieurs approches pour mettre en oeuvre des tests unitaires automatisés : chacune a des avantages et des inconvénients dont il faut tenir compte selon le contexte.
Il est important de définir quand les tests unitaires sont écrits. Plusieurs mises en oeuvre sont possibles :
Il faut développer de préférences les tests unitaires le plus tôt possible. Dans une approche traditionnelle, juste après l'écriture de la méthode. Dans une approche TDD (Test Drive Development), avant l'écriture de la méthode.
Ceci présente plusieurs avantages par rapport à une écriture ultérieure de tests :
Il est préférable d'appliquer trois règles avec les tests unitaires :
Un test unitaire devrait respecter certains principes :
Le rôle des tests unitaires est d'automatiser des tests sur des unités de code les plus petites possibles, généralement une méthode. Cependant le code d'une méthode peut avoir besoin d'autres objets ou de ressources externes.
Plus le code à tester va avoir de dépendances plus il sera difficile à tester. Il faut donc minimiser ces dépendances en utilisant plusieurs solutions :
Un test unitaire doit impérativement se faire de façon isolée, donc sans dépendre d'autres tests ni requérir les dépendances utilisées par la fonctionnalité en cours de test. Le but d'un test unitaire est de tester les traitements de la fonctionnalité et non de tester les interactions qu'elle peut avoir avec ces dépendances.
Un test unitaire doit obligatoirement être répétable pour obtenir toutes ses lettres de noblesse. Cela permet de capitaliser les tests unitaires non seulement pour les tests unitaires mais aussi pour les tests de non-régression.
Chaque cas de tests doit être autonome et ne doit donc pas dépendre d'un ou plusieurs autres cas de tests.
Il est pratique de définir et d'utiliser des conventions de nommages pour les classes de tests. Certaines sont imposées par le framework de tests utilisé : dans ce cas leurs mises en oeuvre est obligatoire. Dans les autres cas, il est préférable de définir ses propres conventions et de les mettre en oeuvre, par exemple :
Plusieurs difficultés sont rencontrées lors de la mise en oeuvre de tests unitaires :
Lorsque l'on parle aux développeurs de rédiger des tests unitaires, il est fréquent d'obtenir des réticences avec des justifications futiles :
Dans la plupart des cas, il est plus difficile d'écrire les tests que d'écriture le code à tester. Ainsi, l'écriture du code d'une application est un art mais l'écriture de tests pour ce code est un art encore plus complexe. De ce fait, la rédaction des cas de tests est fréquemment confiée à des développeurs expérimentés ou dédiés à cette activité.
Les tests unitaires doivent évoluer avec le code de l'application. Il est donc très important que le code des tests unitaires soit simple, compréhensible et maintenable.
La mise en oeuvre de tests unitaires automatisés augmente la fiabilité du code mais elle ne peut pas offrir une garantie à 100% pour plusieurs raisons :
Il n'est pas possible de couvrir tous les cas possibles avec des cas de tests unitaires. Il est donc nécessaire de déterminer quelles classes posséderont des tests unitaires, de maximiser le nombre de ces classes testées, de définir les cas de tests de chaque classes et de maximiser le nombre de ces cas.
Une des grandes difficultés lors de la rédaction de cas de tests est de s'assurer qu'un maximum de cas de tests est implémenté. Il ne faut surtout pas se contenter de ne tester que les cas de fonctionnement standard mais aussi couvrir un maximum de cas de fonctionnement anormal (données invalides, levée d'exceptions, tests aux limites, ...).
Généralement, les tests unitaires possèdent des dépendances vers des ressources externes (fichiers, bases de données, bibliothèques tierces, connexions réseau, ...). L'utilisation de ces ressources dans les tests unitaires doit être évitée car généralement elle limite la répétitabilité des tests et entraîne un surcoût dans le temps d'exécution des tests unitaires.
Malgré ces difficultés, les tests unitaires automatisés ne doivent pas être occultés car ils peuvent améliorer de façon significative la qualité et la fiabilité du code lors de son écriture et surtout de sa maintenance.
La rédaction des tests unitaires devrait suivre quelques recommandations :
Chaque test unitaire doit s'exécuter le plus rapidement possible : le nombre de tests unitaires va croître au fur et à mesure des développements donc le temps d'exécution des tests va croître lui aussi.
Il est préférable d'inclure l'exécution des tests unitaires dans un processus d'intégration continue.
Il faut conserver les cas de tests les plus simples possibles. Par exemple, pour le test d'une méthode qui additionne deux nombres, il est préférable pour tester le cas standard qui utilise de petits nombres plutôt que d'utiliser de grands nombres. La véracité du test est la même mais le test est plus facile à comprendre et à vérifier.
Chaque test doit correspondre à un cas de test unique. Il est préférable de n'avoir qu'un seul test dans un cas de test, soit une seule instruction de type assert. Ceci rendra le code du test plus simple et facilitera le calcul de métriques lors de l'exécution de tests.
Le test d'un constructeur nécessite généralement l'invocation de getters et setters pour vérifier les valeurs des paramètres fournis au constructeur et généralement utilisées pour initialiser directement ou indirectement des champs de l'objet.
Il n'est pas toujours facile de rendre les tests d'une méthode indépendants de l'utilisation d'autres méthodes. Par exemple, il est difficile de tester un setter sans faire appel au getter de la propriété correspondante.
Pour tester des méthodes privées, il faut tester les méthodes qui font appels à ces méthodes privées.
Il est aussi généralement non trivial, de tester une méthode qui n'a pas de paramètre de retour. Ces méthodes effectuent généralement des modifications sur des éléments internes ou externes à la classe. Il faut alors capturer le résultat de ces modifications pour pouvoir réaliser les tests.
Il ne faut pas hésiter à remonter dans le gestionnaire de source du code dont un ou plusieurs tests unitaires échouent.
Il ne faut pas hésiter à enrichir les tests avec de nouveaux cas ou créer des cas de tests pour des classes qui n'en ont pas. Le code d'une application et le code des tests unitaires doivent évoluer dans une optique d'améliorations continues.
A chaque maintenance dans le code, les tests unitaires doivent être exécutés et maintenus eux aussi au besoin. Il ne faut surtout pas livrer du code dont au moins un test unitaire échoue quelques soient les raisons.
De nombreux frameworks et outils open source sont proposés pour faciliter la mise en oeuvre des tests
Plusieurs frameworks open source sont utilisables dans le monde Java notamment :
JUnit est à l'origine de plusieurs frameworks similaires pour différentes plates-formes ou langages notamment nUnit (.Net), dUnit (Delphi), cppUnit (C++), ... Tous ces frameworks sont regroupés dans une famille nommée xUnit.
Généralement les tests unitaires de code d'une application, notamment celles développées en couches, nécessitent l'utilisation d'objets de type mock pour permettre de se concentrer sur le test du code de la méthode en minimisant les effets de bord liés aux autres objets utilisés dans le code.
L'utilisation de ces frameworks est détaillé dans le chapitre «Les objets de type mock» .
JUnit est utilisé dans un certain nombre de projets qui proposent d'étendre ses fonctionnalités :
Apache JMeter est l'outil de tests de charge le plus répandu pour tout ce qui repose sur le protocole http.
SoapUI est particulièrement adapté pour les tests unitaires et les tests de charges de services web.
Des outils sont proposés pour vérifier le taux de couverture des cas de tests vis-à-vis du code (test coverage analyser).
Le but de ces outils est de faciliter la détermination des fonctionnalités qui possèdent des tests et par conséquent permettre de déterminer quelles portions du code ne sont pas testées du tout ou insuffisamment testées.
Plusieurs outils open source existent notamment :
|