IdentifiantMot de passe
Loading...
Mot de passe oublié ?Je m'inscris ! (gratuit)

 

Développons en Java   2.30  
Copyright (C) 1999-2022 Jean-Michel DOUDOUX    (date de publication : 15/06/2022)

[ Précédent ] [ Sommaire ] [ Suivant ] [Télécharger ]      [Accueil ]

 

114. Les frameworks de tests

 

chapitre    1 1 4

 

Niveau : niveau 2 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 :

  • tests unitaires : les tests unitaires automatisés sont un des mécanismes les plus importants pour améliorer la qualité et tenter de garantir la fiabilité du code d'une application.
  • tests d'intégration
  • tests de recette : le but est de vérifier que l'application réponde aux spécifications fonctionnelles. Ces tests sont faits par les utilisateurs qui devraient fournir un PV de recette
  • tests de charge (robustesse, performance, montée en charge, ...)
  • tests de stress
  • tests d'acceptabilité
  • tests de sécurité
  • ...

La mise en oeuvre des tests peut être facilitée par l'utilisation d'outils :

  • frameworks d'automatisation des tests unitaires : xUnit, TestNG, ...
  • outils de couverture de code (code coverage) : EMMA, Cobertura, ...
  • outils pour automatiser les tests des IHM : Selenium pour les applications web, ...
  • outils pour les tests fonctionnels : FitNesse, ...
  • ...

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 :

 

114.1. Les tests unitaires

Les tests unitaires peuvent être réalisés de différentes manières :

  • manuelle : par exemple en utilisant les capacités de l'IDE notamment celles du débogueur
  • manuelle et reproductible : par exemple en créant pour chaque classe une méthode main qui permet d'exécuter des tests. Ce type de tests nécessite un lancement à la main et une analyse humaine des résultats
  • automatisée avec un framework de tests

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 :

  • setup : initialiser des objets ou des ressources
  • call : exécuter le code à vérifer
  • verify : vérifier des données issues des traitements
  • teardown : permettre de faire le ménage ou de libérer des ressources

Les tests unitaires automatisés sont très importants et ce durant tout le cycle de vie d'une application :

  • conception : rédaction de la liste des cas de tests
  • développement : tests du code, détection précoce de bugs,
  • maintenance : tests de non-régression, encouragent et facilitent le refactoring

 

114.1.1. L'utilité des tests unitaires automatisés

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 :

  • améliorer la granularité des méthodes : il est plus facile de tester des méthodes courtes que de longues méthodes
  • réduire la dépendance entre les objets : il est intéressant de mettre en oeuvre certains design patterns afin de réduire le couplage entre les objets
  • une classe avec un couplage fort vers d'autres classes est difficile à tester.

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.

 

114.1.2. Quelques principes pour mettre en oeuvre des tests unitaires

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 :

  • écrire les tests juste après avoir écrit une méthode
  • idéalement, écrire les tests avant le code à tester
  • écrire les tests, écrire le code pour faire échouer les tests, vérifier que les tests échouent, corriger le code, vérifier que les tests sont OK

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 :

  • permettre de détecter des bugs le plus rapidement possible,
  • coder des cas oubliés dans la méthode,
  • s'assurer que les tests unitaires sont écrits, 
  • écrire du code testable évitant ainsi un refactoring parfois conséquent
  • ...

Il est préférable d'appliquer trois règles avec les tests unitaires :

  • tester le plus possible : afin d'augmenter les chances de découvrir des bugs
  • tester le plus tôt possible : plus les tests sont faits tôt plus les bugs sont rapidement détectés
  • tester le plus souvent possible : en les automatisant et si possible en les intégrant dans un processus d'intégration continue

Un test unitaire devrait respecter certains principes :

  • le test doit être le plus petit et le plus simple possible
  • chaque test doit être isolé : un test ne doit pas dépendre d'un autre. Ceci permet aussi de garantir qu'une modification d'un test n'aura pas d'impact sur un autre
  • pour pouvoir être facilement exécutés régulièrement, les tests unitaires doivent être automatisés

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 :

  • utilisation de design patterns
  • utilisation d'objets de type mock
  • éviter de faire appels à la base de données dans les cas de tests
  • ...

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 :

  • préfixer les classes de tests par Test suivi du nom de la classe testée et les mettre dans un package dédié dont le nom correspond au nom du package des classes testés préfixé par test
  • mettre les classes de test dans le même package que les classes à tester en les suffixant par Test. Cela permet entre autres de facilement identifier les classes sans classes de tests. Ant peut être utilisé pour filtrer les classes à inclure dans la génération des livrables
  • écrire une classe de tests par classe testée

 

114.1.3. Les difficultés lors de la mise en oeuvre de tests unitaires

Plusieurs difficultés sont rencontrées lors de la mise en oeuvre de tests unitaires :

  • réticences à la mise en oeuvre
  • difficultés de rédaction et de codage
  • couverture du code testé
  • temps nécessaire à la rédaction des cas tests
  • véracité des cas de tests
  • temps nécessaire à la maintenance des cas de tests
  • les cas de tests doivent être répétables
  • il n'y a pas que le code qui doit être testé, il est aussi nécessaire de tester les valeurs de certaines ressources (base de données, fichiers, ...)
  • ...

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 :

  • "Je n'ai pas le temps",
  • "Je ne sais pas les écrire",
  • "Ce n'est pas mon job",
  • "Je ne fais jamais de bugs",
  • ...

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 :

  • la couverture du code testé ne peut généralement pas être totale
  • il est impossible de couvrir tous les cas de tests
  • les tests unitaires peuvent contenir, eux-mêmes, des bugs

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.

 

114.1.4. Des best practices

La rédaction des tests unitaires devrait suivre quelques recommandations :

  • le nom des tests devrait permettre de facilement fournir une indication sur le but du test
  • il est préférable de n'avoir qu'un seul assert par test car un test ne devrait avoir qu'une seule raison d'échouer
  • le code des tests unitaires doit être maintenu au même titre que le code qu'il teste : la même attention doit être portée dans leur écriture (respect des normes, commentaires, refactoring, ...)
  • stocker les tests unitaires dans un package dédié dont le nom est celui du package de la classe à tester avec le prefixe test

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.

 

114.2. Les frameworks et outils de tests

De nombreux frameworks et outils open source sont proposés pour faciliter la mise en oeuvre des tests

  • frameworks de tests unitaires et leurs extensions
  • frameworks pour le mocking
  • outils de tests de charge
  • outils d'analyse de couverture du test
  • ...

 

114.2.1. Les frameworks pour les tests unitaires

Plusieurs frameworks open source sont utilisables dans le monde Java notamment :

  • JUnit : C'est le plus ancien et le plus répandu ce qui en fait un standard de facto
  • TestNG :

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.

 

114.2.2. Les frameworks pour le mocking

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» .

 

114.2.3. Les extensions de JUnit

JUnit est utilisé dans un certain nombre de projets qui proposent d'étendre ses fonctionnalités :

  • JunitReport : une tache Ant pour générer un rapport des tests effectués avec JUnit sous Ant
  • JWebUnit : un framework open source de tests pour des applications web
  • StrutsTestCase : extension de JUnit pour les tests d'applications utilisant Struts 1.0.2 et 1.1
  • XMLUnit : extension de JUnit pour les tests sur des documents XML
  • Cactus : un framework open source de tests pour des composants serveur J2EE

 

114.2.4. Les outils de tests de charge

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.

 

114.2.5. Les outils d'analyse de couverture de tests

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 :

 

 


[ Précédent ] [ Sommaire ] [ Suivant ] [Télécharger ]      [Accueil ]

78 commentaires Donner une note à l´article (5)

 

Copyright (C) 1999-2022 Jean-Michel DOUDOUX. Vous pouvez copier, redistribuer et/ou modifier ce document selon les termes de la Licence de Documentation Libre GNU, Version 1.1 ou toute autre version ultérieure publiée par la Free Software Foundation; les Sections Invariantes étant constitués du chapitre Préambule, aucun Texte de Première de Couverture, et aucun Texte de Quatrième de Couverture. Une copie de la licence est incluse dans la section GNU FreeDocumentation Licence. La version la plus récente de cette licence est disponible à l'adresse : GNU Free Documentation Licence.