Le principe d’un projet agile est d’effectuer des livraisons fréquentes de façon à ce que le client puisse valider les différentes fonctionnalités au fur et à mesure de l’avancée des développements. De cette façon, il est plus facile de s’adapter à d’éventuels changements exprimés par le client et on évite l’effet tunnel pouvant être rencontré sur des projets non agiles. Les développements vont être répartis entre les différents membres de l’équipe de développement, parallélisés et regroupés pour la livraison à la fin de l’itération. La question qui se pose alors est « Comment s’assurer que tout le code livré est de grande qualité, qu’il ne génère pas d’effet de bord et qu’il n’y a pas de régression par rapport au code existant ? ». La réponse va venir de la mise en place de tests unitaires lors des phases de développement.
Vous avez dit « test unitaire » ?
Il existe différents niveaux de tests dans un projet de développement. On trouve notamment des tests fonctionnels (portant sur l’ensemble d’une fonctionnalité), des tests d’intégration (permettant de vérifier que les différents composants d’une application travaillent correctement ensemble), des tests graphiques et d’ergonomie, des tests de performance ou encore de sécurité. Le point commun de ces différents types de tests est de porter sur une partie ou sur toute l’application, englobant généralement plusieurs fonctionnalités en même temps.
Les tests unitaires sont à part dans la grande famille des tests car ils se placent en amont, au tout début du processus, lors des développements.
Un test unitaire permet de s’assurer du fonctionnement correct d’une partie déterminée d’une application ou d’une partie d’un programme. Il a pour objectif d’isoler le comportement de la partie de code à tester de tout facteur extérieur et de vérifier qu’il est conforme à ce qui est attendu.
Le test unitaire va donc être écrit pour tester une toute petite partie du code source, indépendamment de l’environnement qui l’entoure. Il doit être déterministe, c’est-à-dire qu’exécuté plusieurs fois, il devra toujours retourner le même résultat.
Le test unitaire va décrire ce qu’attend la partie de code à vérifier en entrée (quelles données, dans quel format), ce qu’elle doit faire de ces informations (le traitement) et ce qu’elle doit ressortir comme résultat. Le test est écrit à partir de la description de la fonctionnalité demandée et permet de vérifier son comportement. Si le test passe, le comportement est correct, sinon, le code ne fait pas ce qu’on lui demande.
Un peu d’histoire
Si l’on a parfois l’impression d’en entendre parler depuis peu de temps, le concept même de test unitaire ne date pas d’hier. En effet, c’est en 1994 que Kent Beck créa SUnit, le premier framework de tests unitaires pour le langage Smalltalk. En 1997 est apparu le premier framework de tests unitaires pour le langage Java : JUnit. Cela fait donc plus de 20 ans que les outils existent. On peut se demander pourquoi ils ont mis tant de temps à se présenter comme une évidence dans les projets de développement.
Les frameworks de tests unitaires
Depuis 1997 et l’apparition de JUnit, de nombreux frameworks de tests unitaires ont vu le jour pour les différents langages de programmation, comme PHPUnit pour PHP, Unittest et PyUnit pour Python, JSUnit pour JavaScript ou encore CppUnit pour C++. Tous les frameworks présentent des caractéristiques communes et sont relativement simples à prendre en main. Il suffit généralement de quelques heures ou de quelques jours pour en comprendre la philosophie et créer ses premiers tests unitaires. Ils proposent également un outil, généralement en ligne de commande, permettant d’exécuter un ou plusieurs tests et d’afficher les résultats. Il est ainsi possible d’automatiser facilement l’exécution de suites complètes de tests unitaires pour valider un morceau de code, une fonctionnalité ou l’application dans son ensemble.
Quels que soient la technologie et le langage de programmation utilisés dans un projet agile, il est donc possible de mettre en place des tests unitaires pour le code produit.
A quoi vont servir les tests unitaires dans un projet agile ?
C’est la question que tout le monde se pose tant qu’il n’y a pas encore de tests dans le code. En effet, écrire les tests unitaires demande du travail aux développeurs, qui pendant ce temps, ne développent pas les fonctionnalités proprement dites. Quelqu’un ne sachant pas exactement ce que sont les tests unitaires pourraient facilement se dire qu’il s’agit de temps perdu, puisque le code écrit n’apporte rien en termes de fonctionnalités à l’application, ce qu’attends pourtant le client.
Imaginons maintenant que l’équipe livre une version de l’application à la fin d’un sprint. Des tests ont bien entendu été effectués, pas forcément exhaustifs, mais considérés comme suffisants. Ces tests sont généralement faits à partir de l’interface utilisateur, qui va permettre d’accéder aux différentes fonctionnalités. Le client teste à son tour, en se basant sur un cas limite non prévu par le développeur et obtient une réponse erronée de l’application. S’agit-il d’une anomalie, d’une mauvaise compréhension de la demande ? Pour le savoir, il va falloir analyser l’exécution de l’application dans ce cas précis pour identifier la partie du code concernée et vérifier si son fonctionnement est normal. Si le fonctionnement est anormal, l’erreur provient-elle du code en lui-même, ou d’une autre partie de l’application qui n’a pas fonctionné correctement ? Vous comprenez que cette analyse peut être un travail de fourmi, et prendre du temps.
Si des tests unitaires avaient été mis en place, il aurait été plus facile de déterminer l’origine du problème. En effet, si tous les tests unitaires passent correctement, c’est que le code source réagit exactement comme prévu. Les tests portant des noms explicites (par exemple « testOuvrirFichierRenvoieOkSiFichierExiste » va vérifier que la portion de code testée, lorsqu’elle tente d’ouvrir un fichier, renvoie Ok si ce dernier existe bien), il va être relativement simple d’identifier la partie de code impliquée. Si le comportement décrit par le test est bien celui demandé par le client, c’est peut-être que le besoin a changé. Il faudra alors modifier le code source, ainsi que le test associé, pour coller de nouveau au besoin.
En revanche, si des tests ne passent pas, et sont donc en échec, cela signifie que le comportement obtenu n’est pas celui attendu. Cela peut provenir d’autres parties du code, écrites par exemple par d’autres développeurs, et qui à cause d’un effet de bord, bloquent le fonctionnement normal de l’application. Dans ce cas, aucune livraison ne sera effectuée tant qu’une correction ne sera pas apportée et que tous les tests ne réussiront pas.
Mise en place des tests unitaires
La mise en place des tests unitaires dans un projet agile va dépendre du contexte, et des compétences de l’équipe de développement. Si tous sont habitués à l’écriture de tests unitaires, ça ne devrait pas poser de problème. Si ce n’est pas le cas, il faudra prévoir éventuellement des formations, de la programmation par paire avec un binôme comprenant un développeur familier avec les tests unitaires et un autre en cours de formation, et du temps pour que tout se mette en place.
L’organisation va également être différente suivant le type de projet. Sur un projet qui démarre, il sera facile de mettre en place dès le début des tests unitaires. Dans le cas de la reprise d’un code ancien, tout dépend s’il s’agit de réécrire l’application, ou de le maintenir et de la faire évoluer. Dans le premier cas, les tests seront mis en place au fur et à mesure de la réécriture du code. Dans le second cas, c’est plus délicat, puisque l’on n’a pas le temps de parcourir l’ensemble du code source pour ajouter les tests. Il va donc falloir le faire de façon progressive. Les tests unitaires seront systématiquement ajoutés pour les nouveaux développements. Lorsqu’il s’agira de faire évoluer l’ancien code, les tests seront écrits pour les nouvelles parties du code modifiées. Enfin, lors de la correction d’une anomalie, seul le code impliquant la correction sera couvert par des tests. La suite de tests unitaires grandira donc au fur et à mesure des développements.
Il ne sert à rien non plus de tout tester, et de multiplier les tests unitaires pour le plaisir. Trop de tests tuent les tests, car il faudra évidemment les maintenir et le cas échéant, les faire évoluer en même temps que le code source. Il est important que les tests unitaires couvrent avant tout les fonctionnalités critiques de l’application. Si une portion de code se contente d’additionner des nombres, il y a peu de chance qu’une telle opération échoue et il n’y a aucun intérêt à lui associer un test. En revanche, la portion de code qui prépare les données qui seront utilisées par la suite pour une transaction bancaire sera sans doute critique et devra faire l’objet d’un ou plusieurs tests unitaires.
Les tests unitaires couvriront donc en priorité les cas nominaux, les cas d’erreurs et les cas aux limites.
L’exécution d’un test produit un résultat décrit par le framework grâce à un code : « . » indique que le test est passé, « E » indique une erreur à l’exécution (indépendante du test en lui-même), « F » indique que le test a échoué (au moins une assertion a échoué), « I » indique que le test est incomplet (aucune assertion n’a été trouvé dans le test) et enfin « S » indique que le test a été ignoré.
Les bonnes pratiques
Il est important de garder certaines règles en tête pour que la mise en place des tests unitaires au sein d’un projet agile se passe le mieux possible.
L’idéal est de pratiquer le TDD (ou développement dirigé par les tests), c’est-à-dire que les tests sont écrits avant le code source, et que ce sont eux qui dirigent finalement l’écriture de l’application. Mais si ce n’est pas possible, il faut écrire le code, en pensant aux tests. C’est-à-dire que le code doit être pensé et écrit de façon ce qu’il soit naturellement et unitairement testable.
Il faut veiller à maintenir un couplage faible des composants, c’est-à-dire qu’il y ait le minimum de dépendances entre eux (chaque composant devant être indépendant dans l’idéal). L’utilisation du modèle conception « injection de dépendance » facilitera la mise en place des tests unitaire, justement en limitant le couplage entre composants. Il faut également éviter de propager du code qui peut être refactorisé, car dans ce cas, les tests devront eux-mêmes être multipliés. Dès qu’un partie du code est identifiée comme pouvant être refactorisée, elle doit l’être afin d’éliminer le code dupliqué.
Des revues de code régulière ainsi que la mise en place de programmation par paire (« pair programming ») peut également grandement faciliter la mise en place des tests unitaires, chaque développeur pouvant amener son point de vue sur du code qu’il n’a pas écrit.
Il est également important de respecter une certaine structure dans chaque test, de façon à ce que chaque développeur puisse facilement relire et comprendre un test qu’il n’a pas écrit : il s’agit du modèle « AAA ». La structure classique d’un test est composée de trois parties :
- « Arrange » : initialisation du contexte d’exécution du test unitaire (variables, données, dépendances, bouchons…),
- « Act » : exécution du traitement nécessaire pour la vérification du comportement du code testé,
- « Assert » : vérification du critère de réussite du test au travers d’une assertion.
Enfin, il existe 10 règles communément admises pour l’écriture des tests unitaires :
- Un test doit être unitaire (il est indépendant, ne concerne qu’un seul comportement et ne fait appel à aucun composant extérieur).
- 100% des appels externes du composant sont simulés par des bouchons.
- Un test doit être simple, normé, commenté, facile à lire et à comprendre.
- Un test doit être rapide à exécuter (la suite de tests doit pouvoir être exécutée souvent).
- Les tests doivent être écris le plus tôt possible (il n’y a jamais de « plus tard »).
- Un test = une assertion.
- Les parties critiques de l’application sont testées en priorité.
- Le code de l’application doit être écrit et refactorisé de façon à être testable.
- Utiliser les modèles de conception (ou « design patterns ») injection de dépendance et AAA.
- Le nom d’un test doit être normalisé (la lecture du nom doit permettre de savoir exactement ce que va faire le test).
Avantages et inconvénients
La mise en place de tests unitaires lors du développement d’une application présente de nombreux avantages. Ils permettent notamment de documenter automatiquement le code source. En effet, la lecture des tests permet de comprendre le comportement attendu du code, et donc le fonctionnement de l’application, tant technique que logique.
Les frameworks disponibles permettent l’automatisation de l’exécution d’un test, d’un ensemble de tests et ou de tous les tests de l’application. Ils peuvent donc être exécutés à chaque modification du code, à chaque livraison par un développeur, à chaque sprint… Le fait de s’assurer que l’ensemble des tests passent avec succès quelles que soient les modifications du code effectuées par un ou plusieurs développeurs permet de plus facilement repérer d’éventuelles régressions. Si à la suite d’une modification certains tests sont en échec, cela signifie qu’elle est à l’origine d’une régression. Une nouvelle correction du code de façon à ce que les tests réussissent permettra d’en annuler les effets. Ces tests automatisés représentent donc un formidable outil de non-régression.
Les tests unitaires vont donc permettre aux développeurs de travailler plus sereinement, puisqu’ils pourront sans crainte effectuer les modifications qu’ils jugent nécessaires sur le code source. Ils pourront également supprimer des parties de code inutiles afin d’optimiser l’application, tout en s’assurant de sa non-régression.
La lecture de ces tests facilitant la compréhension du comportement du code source, sa prise en main par de nouveaux développeurs sera plus simple. La maintenance de l’ensemble de l’application s’en trouve grandement facilitée. Les erreurs pourront être repérées plus rapidement. Plus une anomalie est identifiée tôt dans le projet et moins elle coûtera cher à corriger.
Si les tests unitaires présentent de nombreux avantages, leur mise en œuvre a tout de même un inconvénient majeur : elle prend du temps. En effet, certains responsables peuvent penser que le temps passé par les développeurs à écrire les tests est du temps perdu. Tout le code produit pour l’écriture des tests ne fait pas avancer l’application en elle-même, ne fait pas avancer les développements. Néanmoins, tout le temps passé à la création des tests unitaires sera ensuite autant de temps gagné pour la maintenance, la détection et la correction des anomalies pouvant survenir par la suite. Il faut donc bien garder en tête que la création des tests va prendre du temps mais qu’il s’agit d’un investissement pour l’avenir du projet.
Des freins à l’adoption des tests unitaires ?
Il peut exister de nombreux freins à l’adoption des tests unitaires dans un projet agile, pouvant provenir des développeurs comme des responsables du projet.
Le paradoxe, c’est que tous s’accordent pour élever les tests unitaires et le TDD au rang de bonne pratique de développement. En revanche, lorsqu’il s’agit de les mettre en place, tous trouvent de bonnes excuses pour ne pas le faire : nous n’avons pas le temps, le ROI est difficilement identifiable, ça prend du temps, ça ne sert à rien de tester le code puisqu’il fonctionne, c’est trop difficile, ce n’est pas mon rôle…
Il va donc falloir changer les habitudes de développement et de conduite de projet, et prendre conscience des avantages qu’apporte la mise en place des tests unitaires. Oui, cela prend du temps, oui, cela oblige à penser différemment les développements, et oui enfin, il est difficile de quantifier précisément leur apport. Néanmoins, le temps de travail des développeurs est généralement partagé à 20% pour le développement de l’application, et à 80% pour la recherche et la correction des anomalies. Si les tests unitaires peuvent réduire le nombre d’anomalies potentielles et le temps de correction de celles qui subsistent, ces heures économisées pourront servir utilement à l’amélioration de l’application.
Le TDD (« Test Driven Development ») ou développement piloté par les tests
Le développement piloté par les tests est une méthode de développement consistant à écrire les tests unitaires avant même de commencer à écrire le code source de l’application.
Pour faire simple, le développeur va suivre le cycle suivant :
- Ecriture du test (en fonction du comportement attendu du futur code source)
- Lancement et échec du test
- Ecriture du code source le plus simple possible qui permettra d’obtenir le comportement attendu par le test
- Lancement et réussite du test
- Refactorisation du code pour l’optimiser
- Lancement du test et vérification qu’il réussit toujours malgré la refactorisation
L’écriture du test en premier va permettre de s’assurer que le développeur a bien compris le comportement attendu du code. A partir de la description de la fonctionnalité, il va être amené à réfléchir à la façon dont va s’exécuter son futur code, à ce qu’il attend en entrée et à ce qu’il va devoir produire en sortie, ainsi qu’au potentielles dépendances.
Cette méthode va amener le développeur à produire un code plus simple, plus compréhensible et plus optimisé. Le code produit sera au final de meilleure qualité, sa maintenance et son évolution en seront grandement facilitées.
En revanche, cela implique un changement des habitudes de développement au sein des équipes qui ne sont pas rôdées à ce processus, ainsi qu’un coût supplémentaire lié au temps passé à l’écriture des tests.
Néanmoins, les développements pilotés par les tests produisent du code source de meilleure qualité et des applications plus stables, maintenables et évolutives.
La couverture de code
La notion de « couverture de code » va de pair avec les tests unitaires. Il s’agit de la mesure du taux de code source d’une application exécuté lorsque les tests unitaires sont lancés.
Peut-on dire alors qu’une couverture de 100% du code source équivaut à dire que l’application est parfaitement testée ? Absolument pas en fait. La couverture de code, calculée par le framework de tests unitaires utilisé, n’est qu’une mesure quantitative, et non qualitative.
En effet, la couverture de code ne contrôle pas que des assertions soient présentes dans les tests, que les bons comportements soient vérifiés ni que les tests soient réellement unitaires, lisibles et bien écrits.
Trop souvent, on voit le responsable du projet fixer un objectif de 80% de couverture de code, sans vérifier que les bonnes pratiques sont appliquées. Une application peut donc obtenir la couverture demandée, 80% ou même 100%, sans pour autant être réellement testée. Il suffit que les développeurs créent les tests pour répondre à la couverture de code, parce que la direction du projet met une pression trop importante sur eux par exemple, au lieu de tester les chemins critiques, pour que cette couverture ne signifie plus rien.
Il vaut donc mieux dans un premier temps, ne pas donner d’objectifs clairs en termes de couverture de code, mais plutôt vérifier que les bonnes pratiques sont adoptées par l’équipe, et que le nombre de tests unitaires augmente au fur et à mesure des développements. Il est possible de donner comme directive une couverture de 100% pour les nouveaux développements qui sont par définition moins dépendants de l’existant, et la création systématique de nouveaux tests pour les évolutions et les corrections de l’application. Petit à petit, la couverture de code va augmenter, peut-être jusqu’à atteindre ou même dépasser l’objectif courant de 80%, avec l’assurance que les tests créés sont réellement utiles.
Nutcache est l’outil idéal pour gérer votre projet agile. Testez-le gratuitement pendant 14 jours!