Pourquoi développer des tests unitaires ?
Accès rapide :
Pourquoi tester une application ?
Quels sont les différents types de tests ?
Quel est le SUT dans le cadre des tests unitaires ?
Quelques frameworks de tests unitaires
Le framework JUnit
Le framework Mockito
Le framework TestNG
Pourquoi tester une application ?
Au fur et à mesure des années, les applications développées sont de plus en plus conséquentes. Le volume de code à produire suit donc cette accélération.
De plus, une application doit être maintenue pour corriger les bugs et surtout pour y ajouter de nouvelles fonctionnalités. Il en résulte que le code
source de cette application doit être maintenable.
Pour répondre à ce besoin de maintenabilité, l'approche objet aide beaucoup, mais ce n'est pas suffisant. Effectivement, à chaque livraison d'une nouvelle
version, il faut vérifier que les nouvelles fonctionnalités s'exécutent bien correctement. Mais il faut aussi vérifier que ce qui marchait bien précédemment
(sur les versions précédentes), continue à bien fonctionner sur la version courante. Il ne faut pas, en cas de régression (de dysfonctionnement d'une fonctionnalité
précédemment développée), que ce soit l'utilisateur qui constate le problème. Il est donc nécessaire, à chaque nouvelle livraison de l'application,
de vérifier que l'intégralité des fonctionnalités précédentes continue à correctement s'exécuter. Il est nécessaire de pouvoir relancer ces tests :
il faut donc que vous conserviez le code des tests entre les versions pour pouvoir les rejouer.
Le seul moyen de garantir la non régression passe par l'automatisation de l'exécution des procédures de tests (des scénarios de tests si vous
préférez). Pour facilité ce point, vous pouvez opter pour l'utilisation d'un « Test Runner » (JUnit en est un).
Quels sont les différents types de tests ?
Si l'on rentre dans la théorie des tests, il existe différents niveaux de tests : chacun de ses niveaux est associé à une procédure de tests devant
exécuter un grand nombre de tests. D'une procédure de tests à une autre, ce qui change c'est le SUT (System/Software Under Test), nous allons y revenir.
Traditionnellement, on identifie quatre strates de tests (quatre couches).
-
Les tests unitaires (ou tests de composants) : permet de tester une unité fonctionnelle (un composant) du système à développer.
Si cette unité à des dépendances sur d'autres unités fonctionnelles, on met en oeuvre
des bouchons (en anglais on parle de Mock) pour simuler
ces dépendances et vérifier que, seule et isolée, cette unité fait bien correctement son travail.
Pour de plus amples informations, vous pouvez activer ce lien.
-
Les tests d'intégration : cette étape, qui fait suite la procédure de tests unitaires, permet d'intégrer ensemble les différentes unités
(les composants) et de vérifier que les communications inter-composant sont bien correctement réalisées. Normalement, les communications entre composants
sont spécifiées par interfaces, ce qui permet le bouchonnage. Dit autrement, sur une intégration donnée, il se peut que tous les composants ne soient pas
encore développés et grâce au couplage par interface, on peut temporairement intégrer des simulateurs (des bouchons). La procédure de tests
d'intégration a pour but de tester la communication au niveau de ces interfaces inter-composants.
Pour de plus amples informations, vous pouvez activer ce lien.
-
Les tests de validation (ou test de vérification) : on vérifie, une fois tous les composants intégrés et la procédure associée passée en succès,
que le logiciel fait bien ce pour quoi il a été développé.
Pour de plus amples informations, vous pouvez activer ce lien.
-
Les tests de qualification (aussi appelé test d'acceptation ou de recette) : souvent cette procédure de tests est un sous-ensemble de la procédure
de tests de validation. La différence réside dans le fait que la qualification est prononcée en présence du client, avec lequel on valide surtout les
aspects fonctionnels.
Pour de plus amples informations, vous pouvez activer ce lien.
Outre cette classification des procédures de tests, on peut aussi classer les tests en fonction de leur nature.
-
Les tests fonctionnels : on vérifie qu'une fonctionnalité du SUT est correctement réalisée.
-
Les tests techniques : on teste les aspects non fonctionnels. Il existe plusieurs types de tests techniques.
-
Les tests de performance : on cherche à mesurer certaines consommations de ressources (CPU, mémoire, consommation du réseau...).
-
Les tests de robustesse : on cherche à tester des problèmes de stabilités et de fiabilités dans le temps. Souvent ces tests
durent longtemps pour pouvoir mesurer l'évolution d'une ressource (détection de fuites mémoire, scalabilité...).
- ...
Pour chacune des quatre de procédures de tests vu précédemment (tests unitaires, d'intégration, de validation et de qualification) on peut considérer avoir
un ensemble de tests fonctionnels et d'autres plus techniques.
même au niveau d'un test unitaire, il peut être important de vérifier qu'un composant logiciel ne consomme pas toutes les ressources disponibles via des tests
techniques (par exemple, le CPU). Ainsi, après intégration, on sera certains que tous les composants déployés pourront avoir suffisamment de ressources disponible.
Par exemple, un test technique sur composant logiciel pourrait être : « vérifier que le composant ne consomme pas plus de 30% de CPU ».
Un autre test pourrait être : « vérifier que le composant ne consomme pas plus de 200 Mo de mémoire ».
Notez encore qu'on peut classer les tests selon un autre axe :
-
Tests « Black Box » (ou tests boîtes noires) : on considère le SUT comme étant opaque. On envoie des données d'entrée et on vérifie
que les résultats produits sont bien cohérents par rapport aux attendus.
-
Tests « White Box » (ou tests boîtes blanches) : on considère le SUT comme étant ouvert et on y place des sondes à l'intérieur
pour vérifier que les changements d'états dans celui-ci sont bien cohérents par rapport l'état prévu à chaque stade.
On vérifie aussi la cohérence des données produites au final.
Pour clore ces définitions, rappelez-vous qu'un test :
-
Doit vérifier qu'une seule exigence à la fois : les autres exigences seront vérifiées par d'autres tests.
On entend par exigence un aspect à garantir par le logiciel : il peut s'agit d'une exigence fonctionnelle ou d'une exigence technique.
-
Doit produire un résultat « mesurable » : l'objectif étant de vérifier la conformité des résultats aux attendus.
Par exemple, un simple booléen est « mesurable » : soit c'est vrai, soit c'est faux.
Mais cela est d'autant plus vrai dans le cas des tests techniques censés mesurer des consommations de ressources.
-
Doit être le plus rapide possible, à l'exception de certains tests techniques (tests de robustesse notamment).
A partir de maintenant, nous n'allons nous intéresser uniquement qu'à la notion de tests unitaires.
Les autres types de tests ne seront pas appréhendés dans ce tutoriel.
Quel est le SUT dans le cadre des tests unitaires ?
Contrairement à une idée reçue, un test unitaire ne porte pas systématiquement sur une classe (bien que cela puisse être le cas).
Comme nous l'avons dit précédemment, l'unité en test est normalement un composant logiciel : en ingénierie logicielle, un composant logiciel est un élément
constitutif d'un logiciel. Il est développé indépendamment des autres composants. Tous les composants sont ensuite intégrés ensemble pour constituer le
logiciel.
En UML (Unified Modeling Language), un formalisme particulier est proposé pour définir un diagramme de composants.
Un composant y est représenté par un rectangle complété par deux petits rectangles sur son côté gauche.
Imaginons que nous cherchions à développer un site de vente en ligne : le diagramme de composants UML suivant montre comment nous pourrions
architecturer le logiciel. On y constate que les composants sont couplés par interfaces (les ronds) qui constituent les différentes API
(Application Programming Interface) de chaque composant.
les traits pleins entre un composant et une interface correspondent à une implémentation du contrat (de l'interface) par un composant.
Les flèches en pointillées correspondent à des relations de dépendances sur une API (un composant à besoin d'un autre pour son fonctionnement).
il doit être clair qu'un composant n'est pas forcément une unique classe. En termes de Java, un composant serait plus proche de la notion de package :
ce package pourrait contenir plusieurs classes et représenter plusieurs milliers de lignes de code. En termes de déploiement, un composant pourrait être
associé à un JAR (une archive Java).
Ainsi, le composant de gestion des commandes (CommandComponent) a besoin du composant de sécurité et du composant de gestion du stock pour pouvoir fonctionner.
Si l'on souhaite tester uniquement le composant de gestion de commandes, il faudrait donc simuler les autres composants : cela sera possible, car
le couplage entre composants est réalisé par interface. On pourra donc aisément simuler un stock en implémentation un mock à partir de l'interface associée
et en signalant, par exemple, que les articles d'une commande sont systématiquement disponibles (peut-être un simple return true;
dans le code
du mock).
le terme de mock est basé sur le mot anglais « mockery » qui peut être traduit en moquerie ou bien farce. On peut parler de métaphore :
on fait une farce au composant logiciel en lui faisant croire qu'il s'exécute dans la vraie vie alors qu'il tourne en environnement simulé.
Dans un tel développement, il faudrait donc quatre jeux de tests unitaires : un jeu par composant. Pour un jeu de tests donné, le SUT serait donc le
composant considéré. Si les quatre jeux de tests s'exécutent correctement pour chaque composant, on peut alors intégrer ces composants ensemble et
procéder à l'exécution de la procédure de tests d'intégration puis poursuivre.
Quelques frameworks de tests unitaires
En Java, il existe plusieurs frameworks de test unitaires. Loin d'être exhaustif, voici quelques frameworks que vous pourrez rencontrer au gré des projets
sur lesquels vous pourrez intervenir.
Le framework JUnit
JUnit est certainement le framework de test plus utilisé, ce qui explique que dans la suite de ce cours, nous allons l'utiliser.
Historiquement parlant, il y a eut plusieurs versions de JUnit et nous retiendrons les versions JUnit 3.x, JUnit 4.x et JUnit 5.x.
Entre ces trois versions, il y a eut rupture de compatibilité : la mise en oeuvre de tests unitaires se fait différemment en fonction de la version utilisée.
Comme ces trois versions sont encore beaucoup utilisées au gré des divers projets existants, je vais vous présenter les trois approches dans les chapitres
suivants.
bien entendu, si vous êtes nouveau venus dans l'univers Java et si vous n'avez pas d'antériorité en termes de version du JUnit utilisée, je vous propose de
passer directement à l'étude de
JUnit 5.x. Les autres versions sont présentés pour ceux qui sont obligés de travailler avec une
version précédente du logiciel.
Le logiciel peut être téléchargé à l'adresse suivante :
https://junit.org/. Mais la grande notoriété
de JUnit fait qu'il est déployé, par défaut, dans tous les IDE Java majeurs. Par exemple, si vous utilisez Eclipse, vous n'avez pas besoin de le télécharger,
il est déjà là prêt à être utilisé.
Le framework Mockito
Un autre framework de test orienté « Tests Unitaires ». Comme son nom le laisse suggérer, le logiciel propose des outils de génération de
bouchons (de mocks).
Le framework TestNG
Le nom TestNG signifiant « Test Next Generation », il s'agit d'un autre « Test Runner » s'inspirant fortement de JUnit et de NUnit.
Contrairement à JUnit, TestNG n'est pas orienté que « tests unitaires » et permet de plus facilement prendre en charge les autres types de tests
(tests d'intégration, tests de validation et tests de qualification).
Améliorations / Corrections
Vous avez des améliorations (ou des corrections) à proposer pour ce document : je vous remerçie par avance de m'en faire part, cela m'aide à améliorer le site.
Emplacement :
Description des améliorations :