Makina Blog

Le blog Makina-corpus

Mythes et réalités des tests automatisés


Petit bestiaire des mythes autour des tests automatisés et des réalités correspondantes

Le sujet des tests automatisés fait parfois débat dans le monde du développement et certaines idées reçues reviennent assez régulièrement. Je vais tenter ici d'en dresser un inventaire et d'y répondre en me basant sur mon expérience. Il ne s'agit pas de prononcer des vérités définitives. J'espère simplement apporter des éléments à votre propre réflexion sur le sujet.

L'écriture de tests automatisés ralentit le développement

C'est de loin le motif le plus fréquemment invoqué pour faire l'impasse sur les tests automatisés. En pratique on s'aperçoit que ce n'est généralement pas le cas. J'en ai acquis la conviction il y a quelques années en négociant une estimation avec un client lui même développeur. Je n'ai pas mentionné la question des tests dans la discussion et nous nous sommes mis d'accord sur une estimation de la charge de travail uniquement basée sur les fonctionnalités à livrer. J'ai livré dans le budget imparti et mon client fut ravi de découvrir que je lui livrais en même temps une suite de tests. Depuis je procède ainsi, la production de tests n'entre pas dans mes estimations car je considère qu'ils font partie du processus normal de développement.

Sur une itération initiale je dirais que l'impact de l'écriture de tests est le plus souvent neutre en terme de temps de développement. C'est lorsqu'on doit revisiter le code pour le modifier, parfois des mois ou des années plus tard qu'on gagne un temps précieux. On pourrait donc en conclure que si on est certain de ne jamais devoir modifier un projet on peut se passer de tests, ce qui nous amène au point suivant.

Les tests automatisés ne sont pas utiles à court terme

Comme évoqué précédemment, les gains de temps apportés par les tests automatisés ne sont pas toujours significatifs à court terme. Ceci étant dit, en tant que codeur, je préfère passer du temps à écrire des tests automatisés plutôt qu'à tester manuellement le comportement de mon code. D'une manière ou d'une autre, il faudra valider le bon fonctionnement de mon programme et le faire manuellement implique souvent d'effectuer des séquences d'actions répétitives et frustrantes. L'écriture de tests est à l'inverse une activité intéressante et stimulante lorsqu'on est quelqu'un qui aime coder. Ceci a un impact positif sur mon bien être au travail et donc sur ma productivité.

Notons également au passage que pour du code qui implique des calculs mathématiques les tests automatisés font très rapidement gagner du temps. Par exemple le calcul du montant d'une commande avec ses frais de port est quelque chose de fastidieux à tester à la main et on va rapidement gagner du temps à le tester de manière automatisée. Ce type de code est de plus facile à tester car il peut être implémenté de manière purement fonctionnelle et ne nécessite pas de mettre en place un environnement avec des dépendances (base de données, services externes, etc). Tester du code de calcul mathématique est donc la manière idéale de se mettre le pied à l'étrier avec les tests automatisés car le retour sur investissement est flagrant et rapide.

Les tests automatisés rendent les évolutions difficiles

Alors que l'objection précédente niait l'intérêt des tests à court terme, celle-ci invoque des difficultés à long terme, l'idée générale étant qu'à mesure qu'on va modifier le comportement du logiciel, des tests vont se mettre à échouer et on va devoir les mettre à jour, ce qui serait un frein au développement.

Pour répondre à cela on peut rappeler que même en l'absence de tests automatisés, on aurait de toute façon du tester manuellement de nouveau, à la fois les nouveau comportements attendus et les comportements précédents, pour s'assurer qu'il n'y a pas eu de régression. Avec les tests automatisés au moins nous n'avons généralement à modifier que les tests correspondant aux comportements qui changent.

Dans le cas où un test correspondant à un comportement inchangé échoue, cela peut révéler une régression qu'il faudra corriger. Cela peut également être le signe que la mise en place du test est trop étroitement liée à des détails internes du code testé. Si l'évolution entraîne de nombreuses modifications à travers notre code de test, cela révèle une répétition inutile qu'il va falloir factoriser (le code de test n'est pas DRY).

Enfin on peut se demander pourquoi nous n'avons pas commencé par modifier les tests avant de modifier le code. C'est l'approche dite du développement pilotés par les tests (TDD) que nous évoquerons plus loin.

Le type d'application que je développe n'est pas testable

En pratique, c'est rarement réellement le cas et lorsque c'est vrai, on peut généralement tester la majeure partie de l'application et réduire les parties non testables au minimum. Si par exemple on a du mal à tester du code concurrent, cela ne nous empêche pas de tester les tâches de manière séquentielle. Si la bibliothèque d'IHM qu'on utilise ne facilite pas les tests, cela ne nous empêche pas de tester la logique métier qu'on aura pris soins d'encapsuler avec un pattern comme MVC. Gardons nous du « tout ou rien », il vaut mieux tester les trois quarts de l'application que ne pas la tester du tout. N'utilisons pas le fait de ne pas pouvoir tout tester comme prétexte pour ne rien tester.

Il faut tester son code unitairement, en parfaite isolation du reste du système

Parmi celles et ceux qui pratiquent les tests automatisés, on assiste à des débats concernant les types de tests qu'il faut produire. C'est le sujet de cette section et de la section suivante.

Certains préfèrent tester de petites unités de code individuellement, en remplaçant les composant dont ces unités dépendent par des succédanés communément appelés mocks. Si on applique cette approche de manière stricte, chaque test n'entraîne l'exécution que d'une partie bien délimitée du code, généralement une méthode ou une fonction. On s'assure également que les tests ne sollicitent aucune ressource externe exigeant des entrées/sorties, comme une base de données ou un système de fichier. Cette approche est motivée à la fois par les gains de performance qu'elle permet et par le principe de responsabilité unique, le raisonnement étant que si le code est facile à tester de manière isolée, cela signifie que le code testé est correctement découplé.

Poussée à l'extrême, cette manière de tester peut avoir un impact significatif sur le code testé que l'on va souvent vouloir structurer de manière à faciliter l'injection de dépendances. Cette influence des tests sur le code testé est parfois vivement critiquée. On lui reproche également son manque d'efficacité à détecter les régression car elle ne valide pas l'utilisation du code en condition réelle. Elle peut aussi accroître les problèmes de mise à jour décrits à la section précédente.

Je pense pour ma part qu'il n'est pas utile de chercher à écrire systématiquement des tests parfaitement unitaires. Il faut utiliser les tests unitaires lorsqu'on a besoin des avantages qu'ils procurent, à savoir la rapidité d'exécution et la possibilité de tester certains chemins de code difficiles d'accès, comme par exemple du code de gestion d'erreurs. Pour obtenir plus rapidement une couverture importante et utile je n'ai pas de scrupules à écrire des tests de plus haut niveau.

Les tests unitaires sont une perte de temps, il suffit d'écrire des tests d'intégration

Certains développeurs se détournent de la tendance à tester de petites unités de manière isolée et affirment la supériorité des tests de bout en bout ou tests d'intégration. Ces tests visent à automatiser des scénario complets d'utilisation du point de vue utilisateur. L'objectif est ici de se concentrer sur la validation du comportement observable, le seul qui compte réellement en fin de compte. Ce type de test est assez lourd à mettre en œuvre puisqu'il faut à chaque exécution initialiser un environnement vierge de manière à avoir des tests reproductibles. Comme l'exécution de ces tests traverse toute la pile applicative, il peut être très difficile de localiser la source des échecs. À cause des contraintes d'initialisation et de la profondeur d'exécution de ce type de tests, ils sont lents et on aura naturellement tendance à les exécuter peu fréquemment.

En pratique je n'ai jamais vu ce type de tests utilisé de manière sérieuse et systématique. Il me semble que les inconvénients découragent les développeurs qui ne jouent pas le jeux. Parfois j'ai le sentiment que la prétendue supériorité des tests de bout en bout par rapport aux tests de plus bas niveau est utilisée davantage comme un argument contre les tests de bas niveau que pour motiver réellement l'écriture de tests de bout en bout et en fin de compte aucun test n'est écrit.

Si on utilise des mocks, c'est qu'on teste une implémentation au lieu de tester une interface

Cette affirmation repose souvent sur une représentation incomplète de ce qu'est un système informatique. Si on considère qu'un logiciel ne s'interface qu'avec des utilisateurs alors effectivement l'utilisation d'objets mock et d'assertions sur ces objets parait bien éloignée de l'objectif qui est de répondre aux besoins des utilisateurs. Il suffirait donc de simuler l'interaction entre l'utilisateur et le système testé.

Or en réalité la plupart des systèmes informatiques s'interfacent avec des utilisateurs mais également avec des systèmes externes. Tester consiste à vérifier qu'une interaction entre le système testé et le monde extérieur se passe comme prévu. Les interactions avec des entités autres que des utilisateurs sont aussi importantes. Simuler et vérifier le bon fonctionnement de ces interactions est donc légitime et utile. On peut parler d'entrées et de sorties indirectes (indirect input/indirect output. Les mocks constituent des points de controle pour valider ces interactions.

Il y a pu y avoir parfois des excès d'utilisation des mocks liés au désir de tester son code de manière très isolée. Mocker une classe faisant partie du même projet que la classe qu'on teste me parait excessif. En revanche mocker un service externe comme une API RESTful ou même dans certains cas une base de données peut être pertinent.

Si on a pas 100% de couverture, ce n'est pas la peine d'écrire des tests

Encore une fois, gardons nous du « tout ou rien ». Avoir ne serait-ce que 50% de couverture de test est déjà un énorme progrès par rapport à l'absence totale de test. Plus que le score de couverture, il est intéressant de savoir quel code est couvert et quel code n'est pas couvert. Par exemple si du code d'initialisation d'une application, situé tout en amont de la pile d'appel du framework, n'est pas couvert, ce n'est pas forcément grave car ce code est exécuté tout le temps et donc on s'apercevra très rapidement si il y a un problème. Pour prendre un autre exemple, un code trivial fournissant une fonctionnalité non-critique pourra éventuellement se passer de test. En revanche, du code de gestion d'erreur qui est très rarement appelé en situation réelle gagnera grandement à être testé automatiquement. Je préconise donc d'utiliser les outils de couverture dans une démarche qualitative plutôt que quantitative.

Le code de test n'a pas besoin d'être propre, le copié-collé y est admis

On lit parfois que les tests n'aurait pas besoin du même soin que le reste du code et que le principe DRY ne s'y appliquerait pas. C'est à mon avis une erreur car, comme tout code, le code de test va devoir être maintenu et du code redondant est plus difficile à maintenir. Il ne faut donc pas hésiter à extraire ses propres méthodes d'initialisation ou ses propres méthodes d'assertion. Cela permet d'introduire des noms explicites rendant le code de test plus lisible.

Il existe bien toutefois un point sur lequel le code de test diffère du code normal : pour rester très facile à comprendre il doit rester linéaire. Il faut donc éviter d'y introduire des boucles ou des conditions, quitte à ce qu'il soit moins concis.

Le TDD consiste à écrire tous les tests avant de coder

Les trois derniers mythes que nous allons évoquer concernent spécifiquement le développement piloté par les tests (TDD).

Les développeurs n'ayant jamais pratiqué le TDD s'imaginent parfois (ce fut mon cas) qu'il s'agit d'écrire tous les tests avant d'écrire le code correspondant. Il leur parait à juste titre très difficile d'anticiper tout le comportement attendu de leur code avant d'avoir commencé à l'écrire. En réalité, le TDD est une approche itérative qui procède par petites étapes. On commence par écrire un seul test qui échoue forcément puisqu'on a pas écrit le code, puis on écrit le code pour faire passer ce test, puis on écrit un autre test, puis le code correspondant et ainsi de suite. Je ne vais pas expliquer davantage ici comment faire du TDD, des ressources existent ailleurs pour cela, mais retenons qu'il ne s'agit pas du tout d'écrire tous les tests avant de coder.

Le TDD n'est pas utile, l'important c'est d'avoir des tests

Certains développeurs considèrent que le TDD en soi n'est pas utile mais qu'il importe seulement d'avoir des tests, peu importe à quel moment ils sont écrits.

En pratique, on constate que ne pas faire de TDD est le meilleur moyen de ne jamais écrire de tests. En effet, une fois qu'un code fonctionne, qu'on l'a validé manuellement, on a très peu de motivation à écrire les tests et c'est bien normal, pourquoi s'embêter à tester quelque chose qui fonctionne ? On sera au contraire tenté de passer aux tâches suivantes qui nous attendent et sont autrement plus pressantes. Avec le TDD, l'écriture des tests s'entremêle avec l'écriture du code applicatif, on a pas besoin d'allouer du temps spécifiquement pour les écrire. Ce temps s'intègre naturellement au processus de développement et, lorsqu'on a terminé de développer les fonctionnalités, les tests sont là.

Un autre avantage du TDD est qu'il incite à penser en terme d'interface avant de descendre dans les détails d'implémentation. Lorsqu'on teste une fonction par exemple, on va d'abord réfléchir aux paramètres qu'on lui passe et à ce qu'elle renvoie. Si on teste une API RESTful, on va penser aux verbes HTTP utilisés et à la représentation des données transférées. On décide d'abord du comportement observable de l'entité qu'on s'apprête à développer avant de coder la logique interne. Cette manière de procéder du haut niveau vers le bas niveau peut aider à concevoir de meilleures abstractions.

Si on ne fait pas de TDD, on n'est pas un développeur professionnel

Le TDD est un processus tellement différent du processus classique qu'il a pu faire l'objet d'une forme de dogmatisme dans certains milieux. Des développeurs pratiquant le TDD ont pu afficher une sorte de mépris à l'égard des autres. Cette attitude est à mon avis regrettable et ne donne pas une bonne image du TDD. Il est évident que de très nombreux projets importants et utiles ont été développés sans faire de TDD. Il n'est simplement pas réaliste de considérer a priori les développeurs ne faisant pas de TDD comme mauvais ou non qualifiés. Le TDD est pour moi un outil puissant qui facilite grandement l'écriture de code de qualité, mais il faut bien reconnaître que de très nombreux développeurs s'en passent très bien. On peut faire le parallèle avec l'utilisation d'un IDE. Certains développeurs sont effarés de voir que d'autres utilisent « encore » Vim ou Emacs alors que d'autres considèrent que les éditeurs de texte sont bien plus efficaces. Personne n'a tort ou raison, chaque individu a un fonctionnement mental qui lui est propre et doit choisir les outils qui lui conviennent.

Même lorsqu'on a adopté le TDD, on est pas obligé de le pratiquer tout le temps. Pour ma part je l'utilise la plupart du temps mais lorsque je ne vois pas comment écrire un test avant d'avoir écrit le code, généralement parce que je n'ai pas la connaissance des pré-requis nécessaires pour mettre en place l'environnement de test, je n'ai pas de scrupules à écrire le test après le code. Il m'arrive aussi lorsque je modifie ponctuellement du code existant qui n'est pas testé de ne pas écrire non plus de test car le temps passé à mettre en place l'environnement de test ne serait pas justifié. En revanche si je dois modifier un code existant de manière conséquente, j'essaye de mettre en place des tests pour éviter les régressions. C'est également un bon moyen de se familiariser avec le code.

Conclusion

J'espère que ce petit inventaire vous a intéressé en faisant écho à des choses que vous avez lues ou entendues. Le message général que je souhaite faire passer est qu'il vaut mieux se méfier des solutions miracles et des attitudes radicales. Au contraire, faisons preuve de discernement pour appliquer les différentes pratiques de manière adaptée à nos projets. Tests unitaires, tests de bout en bout, mocks, analyse de couverture ou TDD ont tous leur place dans la boite à outil du développeur qui saura en tirer parti avec pragmatisme.

Pour en discuter, rendez-vous sur Human Coders News.

Formations associées

Formations Python

Formation Python avancé

À distance (FOAD) Du 4 au 8 novembre 2024

Voir la formation

Formations Django

Formation Django avancé

À distance (FOAD) Du 9 au 13 décembre 2024

Voir la formation

Actualités en lien

13/03/2019

Bien configurer ses tests Python avec tox et Travis

Le plus difficile dans le développement des tests unitaires c'est souvent de se motiver à écrire les premières lignes… Alors qu'une fois que c'est initié, ça devient très simple d'en ajouter. Nous verrons dans cet article de blog comment le faire rapidement avec tox et Travis !

Voir l'article
14/03/2016

Bien commencer avec Behat

Initialiser les tests avec Behat pour un projet Drupal

Voir l'article
22/06/2015

Optimiser ses tests unitaires Django avec setUpTestData

Découvrez comment gagner en efficacité sur les tests unitaires et sa méthode d'initialisation des tests : setUpTestData.

Voir l'article

Inscription à la newsletter

Nous vous avons convaincus