1. Définitions
Je ne veux pas rentrer dans les détails, et encore moins les querelles techniques, que les puristes m'en excusent. Je préfère être plus schématique pour faciliter la compréhension du propos. Et ce précisément parce que j'ai pu constater que la défiance généralement observée à l'endroit des tests unitaires vient principalement du fait que l'on ne comprend pas vraiment de quoi il s'agit concrètement.
Pour bien comprendre donc de quoi l'on parle je vais tenter de définir ce type de tests par opposition à ceux que l'on pratique intuitivement, ou plus exactement que l'on est bien obligé de pratiquer au cours du développement, à savoir les tests manuels. Car c'est en effet là que réside la différence principale entre ces deux familles de tests : l'une est manuelle, l'autre est automatisée. Cette caractéristique est primordiale dans le concept de test unitaire. Nous verrons bien sûr que ce n'est pas tout, mais lorsque l'on a compris l'intérêt de l'automatisation, les autres concepts véhiculés par le test unitaire prennent tout leur sens de manière assez évidente.
Revenons donc à nos deux types de tests, et essayons de les caractériser :
- les tests "manuels"
- écriture
- on y procède le plus souvent en ajoutant des petits straps de code à l'intérieur du code de l'application, à grand renfort de var_dump, print_r, echo et autres die
- exécution
- ils s'exécutent en faisant tourner une application ou l'une de ses sous-parties (page, composant, ensemble de composant, etc.) dans son contexte final (i.e. en utilisant l'interface graphique)
- validation
- le résultat attendu est comparé au résultat obtenu par l'opérateur du test
- les tests "automatiques"
- écriture
- ils sont écrits sous forme de classes dédiées, indépendantes de l'application, à l'aide d'un framework spécialisé, offrant toutes les méthodes nécessaires pour matérialiser sous forme de code ce que l'on veut tester (assertions)
- exécution
- ils sont exécutés dans un environnement neutre, isolé, lui aussi indépendant de l'application
- validation
- le résultat attendu est écrit à l'avance, dans la classe de test, et lors de l'exécution, seule la vérification (ou non) de ce résultat attendu comparé à celui obtenu est reporté
2. Conséquences
Reprenons les trois points déterminants utilisés dans la section précédente pour comparer les deux approches :
- Ecriture
- Dans l'approche manuelle, il est nécessaire de modifier le code de l'application elle-même pour y insérer nos tests. Ceci rend par définition le code nécessaire au test éphémère, puisqu'il n'est pas question de le conserver de manière pérenne (par exemple dans son dépôt Subversion), bien qu'il soit à peu près impossible de ne pas oublier un echo ou un var_dump quelques part avant de sauver voire de déployer son code...
- L'approche automatisée permet de déporter le code nécessaire au test dans une arborescence dédiée, et ne vient donc pas polluer l'application.
- Exécution
- Le caractère intrusif de l'approche manuelle expliqué ci-dessus implique qu'il existe un risque que le test lui-même altère le comportement de l'application, et donc le résultat du test. En outre, puisque ce code de test ne peut être conservé de manière persistante, il sera nécessaire de le supprimer puis de le réécrire à chaque fois qu'il sera nécessaire de modifier le composant, et donc de le tester de nouveau
- A l'inverse, ces problèmes ne peuvent pas exister avec des test automatisés. Puisqu'ils sont écrits et maintenus parallèlement au code de l'application, ils n'en modifient jamais le comportement, et peuvent être rejoués à tout moment et en quelques secondes, ce qui est essentiel pour s'assurer tout au long du développement de l'application que ce qui fonctionnait à un moment fonctionne toujours à un autre moment.
- Validation
- Durant un test manuel, on vérifie que le code a produit le résultat auquel on s'attendait, laissant parfois l'interprétation des résultats à la subjectivité du testeur, ou même à sa simple concentration, à son attention du moment...
- Dans un test automatique, on vérifie que le code a produit le résultat que l'on voulait ! Ce qui pourrait sembler être un détail est en fait essentiel. Il est objectif, ne dépend pas de l'opérateur. Il ne dépend que des assertions décrites dans la classe de test, qui ne changent ni avec le temps, ni avec l'opérateur.
3. Du test automatisé au test unitaire
Mais alors pourquoi parler de tests unitaires, et non pas simplement de tests automatisés ? Je ne doute pas que les explications ci-dessus vous auront convaincu de l'intérêt de cette approche, mais peut-être vous demandez-vous encore où est le piège ? Si c'était si simple, les tests unitaires, tout le monde les utiliserait depuis longtemps !
Si ces tests automatisés sont appelés "tests unitaires", c'est tout simplement parce qu'ils ne peuvent s'appliquer qu'à des fractions élémentaires de l'application ("unités"), puisqu'ils sont exécutés en-dehors. En règle générale, l'unité retenue pour un test est la classe, les assertions s'appliquant au retour des méthodes. Pour que chaque unité soit indépendante des autres, il faut ... qu'elle n'en dépende pas !
Prenons un exemple simple : une méthode qui doit travailler sur un objet X doit le recevoir en argument, et non pas l'instancier elle-même (principe de l'injection de dépendance). Au moment de tester cette méthode, elle sera invoquée avec en paramètre cet objet X que l'on aura pris soin d'instancier soit même dans le code du test, avec des données "en dur", et donc que l'on maîtrisera complètement. Ainsi, si le test échoue, on pourra éliminer l'hypothèse que peut-être l'objet X en question n'était pas ce qu'il aurait dû être, etc. En résumé, l'échec d'une assertion ne doit pouvoir signifier qu'une seule chose : l'échec de la méthode testée !
Tester unitairement une application suppose que les classes qui la composent soit fortement découplées. Cela implique une vision très objet du code, qui n'est pas évidente à maîtriser. Et c'est probablement ce qui rend l'adoption massive de la pratique des tests unitaires si délicate.
Pourtant, à bien y réfléchir, on peut aussi voir le code utilitaire comme un guide facilitant grandement la conception des objets composant une application ! En utilisant la phase d'écriture des tests comme support à la conception des objets eux-mêmes, avant même d'en écrire le code, vous vous obligez à créer des objets "propres", très faiblement couplés.
Cerise sur le gâteau, écrire vos tests avant d'écrire les objets vous donne une métrique précise de l'avancement de votre projet : le but étant d'écrire du code jusqu'à ce qu'il permette de faire tourner votre jeu de tests sans que plus aucun test n'échoue, la sortie de l'utilitaire de test vous informera avec précision du nombre de méthodes/objets finalisés, et conformes à vos spécifications (que matérialisent vos tests unitaires). En passant, pour ceux qui ne l'auraient pas identifiée, cette méthodologie est appelée Test Driven Development (dévelopement dirigé par les tests).
Enfin, il est important de préciser que cette méthode, comme les tests unitaires eux-mêmes, doivent en priorité s'appliquer aux parties les plus critiques de l'application, et tout particulièrement la bibliothèque d'objets métiers (ceux qui manipulent vos données !).
Conclusion
Si certains pensent qu'écrire des tests unitaires représente une perte de temps, que l'on ne peut se l'offrir si l'on est pressé, je dirais que ceci n'est (partiellement) vrai que si et seulement si l'on n'est pas à l'aise avec l'écriture de test. Mais ceci, comme tout autre connaissance, vient avec la pratique. Et une fois la compétence acquise, on se rend compte qu'il n'est simplement pas raisonnable de développer sans tests unitaires, ou à tout le moins sans tests automatisés. Pour acquérir cette maîtrise, vous pouvez donc commencer par utiliser un framework de tests unitaires tel PHPUnit (le vénérable ancêtre) ou Atoum (le challenger) pour automatiser vos tests, quels qu'ils soient, et plus vous l'utiliserez, mieux vous le connaîtrez, plus vous affinerez votre code pour finalement créer un code totalement découplé, testable unitairement, plus facile à debugger, à maintenir et à faire évoluer !
2 commentaires:
Gauthier, merci d'avoir trouvé le temps de rédiger clairement ces "évidences".
On est très souvent confrontés à des idées reçues qui contrastent avec ces évidences. Par exemple j'ai entendu dire plusieurs fois que écrire un code testé unitairement coûte 20% de plus qu'un code sans test. Naturellement on ne m'a jamais su citer les sources d'une telle statistique.
Pour ma part, j'appui fortement ton dernier argument : "ceci n'est (partiellement) vrai que si [...] l'on n'est pas à l'aise avec l'écriture de test".
Je voudrais donc dire : faites l'effort de faire monter en compétence vos équipes sur les tests automatisées et vous nous remercierez, au moins à partir de votre deuxième projet
Merci blacksun pour ton commentaire - je pense que la prochaine étape sera d'expliquer pourquoi l'intégration continue est la suite logique de tout ça :)
Enregistrer un commentaire