Contre les extrémismes coûteux (le retour)

Il y a un peu plus d’un an, j’ai écrit un article dans lequel je dénonçais les coûts générés par les intégristes des tests unitaires. Le but de cet article n’était pas de dire que les tests unitaires ne servent à rien − loin de là − mais de pointer le fait qu’une approche pragmatique est préférable à une approche systématique.

Il avait suscité quelques commentaires plutôt positifs, et une réaction très véhémente (avec l’emploi du mot « bullshit » difficile à digérer).

J’en reparle aujourd’hui pour pointer un article intéressant, écrit par David Heinemeier Hansson (créateur de RubyOnRails) et publié sur le blog de 37signals.

Son propos est assez similaire au mien : les tests unitaires, c’est bien ; le test driven development, c’est pratique ; mais chercher à couvrir 100% du code est inutile et coûteux.

Il dresse une liste de sept raisons pour lesquelles il faut se retenir de tester (de son propre aveux, sa liste manque de nuance, mais c’est pour la clarté de son propos) :

  • Ne pas chercher à couvrir 100% du code.
  • Si votre ratio code/test est de 1:2, ça ne sent pas bon ; s’il est de 1:3, ça pue carrément.
  • Si vos tests vous prennent plus d’un tiers de votre temps, vous vous y prenez sûrement mal. Si ça vous prend plus de la moitié de votre temps, vous êtes certain de mal vous y prendre.
  • Ne testez pas le comportement normal de votre ORM.
  • Gardez les tests d’intégration pour valider les éléments séparés (ne pas faire de tests d’intégration sur des éléments qui peuvent être testés unitairement).
  • N’utilisez pas Cucumber à moins de vivre dans le royaume merveilleux des tests-écrits-par-des-non-programmeurs (et envoyez-moi une bouteille de poussière de fée si vous y êtes).
  • Ne vous forcez pas à faire du TDD sur tous vos contrôleurs, tous vos modèles et toutes vos vues (mon ratio est typiquement de 20% de code testé avant d’être écrit, 80% testé après avoir été écrit).

Il cite ensuite une remarque faite par Ken Beck (inventeur de l’extrem programming et auteur référent en TDD) en réponse à une question sur le site StackOverflow :

Je suis payé pour faire du code qui fonctionne, pas pour faire des tests, donc ma philosophie est d’écrire aussi peu de tests que nécessaire pour atteindre un certain niveau de confiance (je suspecte que ce niveau est élevé par rapport aux standards industriels, mais ce n’est peut-être que de la fierté mal placée). S’il y a un type d’erreur que je ne fais habituellement pas (comme remplir les mauvaises variables dans un constructeur), je ne le teste pas.

(je vous conseille de lire le fil complet sur StackOverflow)

Au final, mon but n’est pas de fanfaronner (l’avis de David H. Hansson ne vaut pas forcément plus que l’avis d’un autre), mais juste de répéter ce que j’ai toujours dit : Même le meilleur pattern peut devenir un anti-pattern quand il est mal utilisé ou quand il est utilisé à outrance. La réponse est rarement dans les extrêmes ; on vit dans un monde nuancé.
Et il est illusoire de ne pas prendre en compte les coûts induits par les tests − tout comme il est illusoire de ne pas prendre en compte les coûts de maintenance provenant de code non testé.

13 réponses sur “Contre les extrémismes coûteux (le retour)”

  1. Le plus simple ne serait-il pas de faire un exemple?
    Suggérer qu’à priori, il est inutile de tester des trucs comme les accesseurs/mutateurs simples sans aucune logique métier dans un modèle. Je me trompe ?

  2. J’avais lu lors de sa sortie le billet de David H. Hansson, et j’avais souri à l’époque en repensant (entre autre) à notre discussion.
    Et si je suis d’accord avec certaines choses, je ne suis pas d’accord avec d’autres.
    Lorsqu’il dit qu’il est inutile de chercher à à couvrir 100% du code, de quel code parle-t-il ?
    S’il parle de celui d’une classe métier, je ne suis pas d’accord, car mon expérience me permet de dire que 90% du temps, mes bugs sont dans des parties non testées de mes classes.
    Par contre, s’il parle de l’intégralité du code d’un projet, alors je suis d’accord avec lui (cf plus loin dans ce commentaire).
    La phrase « Si votre ratio code/test est de 1:2, ça ne sent pas bon ; s’il est de 1:3, ça pue carrément » m’a fait sourire car, tout d’abord, en ligne « brute », j’ai bien souvent entre deux et trois fois plus de lignes de test que de code, et de plus, le nombre de ligne est une métrique qui ne veut rien dire dans ce contexte (ni dans un autre, d’ailleurs), si tant est qu’il parle bien de nombre de lignes.
    La phrase suivante, relative au temps passé à écrire les tests, est d’ailleurs de la même veine, puisque je passe bien plus de temps à écrire mes tests que le code permettant de les faire passer, mais c’est peut être parce que le fait d’écrire les tests permet à mon code se construire naturellement.
    À contrario, lorsqu’il dit qu’il ne faut pas testez (par exemple) l’ORM, je suis d’accord, vu qu’il est censé avoir été testé par ses concepteurs (ou pas, mais c’est un autre débat).
    Je suis également d’accord sur sa conception des tests d’intégrations, puisqu’un objet qui se suffit à lui même se comportera certainement de la même manière lorsqu’il est utilisé avec d’autres.
    Concernant Cucumber et les autres outils du même type, je suis plus nuancé que lui, car de mon point de vue, ces outils sont destinés à être utilisé par une paire, à savoir un développeur et un expert métier, et avant le développement proprement dit.
    Dans ce contexte, sa remarque n’a donc pas lieu d’être.
    Je suis également d’accord avec le fait qu’il est inutile de faire du TDD sur les contrôleurs, modèles, vue, car ce n’est pas du métier, juste de la glue pour atteindre un objectif métier, et qu’en plus, cette glue est censée avoir été testée par les concepteurs du framework sous-jacent, tout comme l’ORM.
    Au final, je pense que tout le débat vient du fait que personne ne parle de la même chose de la même façon.
    Une couverture de code de 100% sur l’intégralité d’un projet n’a pas de sens.
    Par contre, sur une classe métier, elle en a.
    Faire du TDD sur la conception d’une classe métier a du sens.
    Par contre, le faire lors de l’intégration d’une classe métier au sein d’un framework n’en a pas.
    Faire du BDD (comprendre du Cucumber/Behat/GreePepper/etc) en phase de spécification a du sens mais le faire en post-développement n’en a pas.
    Tester unitairement un ORM, ou n’importe quel outils tiers (les fameux vendor) n’a pas de sens, par contre s’assurer qu’il se comporte correctement une fois qu’il est intégré dans un projet, donc via des tests d’intégration, a du sens.
    Expliquer un peu tout cela est d’ailleurs la raison d’être et l’un des objectifs de la conférence « Anatomie du test » que je donnerais avec Ivan Enderlin au prochain forum PHP.
    Le test ne se résume pas au test unitaire, et le test unitaire n’exclue pas le test d’intégration ou fonctionnel, et le test automatisé ne rend pas inutile le test manuel.
    Le test fait partie (ou en tout cas devrait faire partie) intégrante du développement, et son coût ne devrait pas d’ailleurs pas en être dissocié, car faire des tests, c’est développer !
    Quand au temps passé et aux métriques, il est déjà difficile de leur donner une signification dans le cadre du développement, je ne m’avancerais donc pas à leur en donner dans le domaine du test.
    Plutôt que de dire que nous vivons dans un monde nuancé, je dirais donc qu’il faudrait commencer par faire les choses correctement en utilisant au bon moment les bons outils afin d’obtenir un flux de production optimisé, aussi bien qualitativement que financièrement (et l’un ne vas pas sans l’autre).
    Tout cela mériterais encore une fois un billet dédié 🙂

  3. @mathrobin : ce n’est pas aussi simple, j’ai déjà eu des bugs dans des accepteurs/mutateurs simples que je n’avais pas testé car considéré comme « simple », justement, et que je n’aurais pas eu si j’avais fait l’effort d’écrire le test.
    Personne n’est à l’abri d’une faute de frappe ou d’une erreur d’inattention.
    De plus, les tests (unitaires) sont potentiellement une documentation de l’API de la classe (code don’t lie), donc omettre ces tests revient à omettre de documenter une partie de la classe.
    Mais c’est une politique d’extrémiste ;).

  4. C’est vrai que tant qu’à faire, si je peux couvrir 100% du code, je préfère le faire (ce qui n’a jamais été le cas de toute façon). Mais je ne priorise jamais les accesseurs/mutateurs que je juge simple, préférant d’abord tester tout ce qui est vraiment sur le chemin critique côté métier.
    Petite question, est-ce qu’il y a un terme, une façon de faire ou toute autre chose qui aurait des grandes lignes directrices plus ou moins reconnues pour la chose suivante :
    Je veux tester l’intégrité de mes données en base. Exemple: J’ai des éléments qui sont catégorisés mais je n’ai pas de clé étrangère (oui je sais, c’est mal, mais j’ai d’autres cas d’intégrité à vérifier et qui sont plus complexes à expliquer). Du coup, je fais des méthodes qui ne servent à rien vis à vis de l’appli elle-même mais qui me permettent par exemple de récupérer la liste des éléments non-catégorisés ou catégorisés dans une catégorie inexistante. Puis je me sers de l’outil de tests unitaires pour m’assurer que ces méthodes me retournent des tableaux vides. Si ce n’est pas le cas, c’est que mes données sont corrompues. Je suis aussitôt averti et peut donc agir assez rapidement.
    Est-ce que c’est une bonne pratique ? Y a t’il plus pertinent ? Y a t’il des choses à savoir sur ces choses ? (désolé, j’ai plein de questions de débutant :p)

  5. @MathRobin Tout dépend si tu veux tester l’intégrité de tes données au cours du développement ou bien en production.
    Si c’est au cours du développement et que tu passes par un outils tiers (comprendre non home-made) pour insérer les informations dans ta base de données, ce n’est pas le rôle des tests unitaires de contrôler ce genre de choses, mais aux tests fonctionnels.
    Cependant, si tu utilises ton propre code pour mettre tes données en base et non un outil du marché type ORM ou un équivalent, ça serait plutôt le rôle de la pile tests unitaires + tests d’intégration + tests fonctionnels.
    Et si c’est en production, alors c’est une fonctionnalité de ton applicatif, et dans ce cas, ton utilitaire de contrôle d’intégrité devrait être développé à l’aide de tests unitaires au niveau de ses classes, de tests d’intégration pour vérifier que le code assemblé fonctionne comme il le doit, et enfin de tests fonctionnels et manuels pour valider le fait qu’il se comporte comme il le doit.
    Maintenant, si tu as bien fait ton boulot durant le développement, il n’y a pas de raison pour que tes données se corrompent en production, n’est ce pas 😉 ?
    En fait, ta question illustre très bien qu’en fonction du contexte de développement, la stratégie de test peut être très différente et qu’en conséquence, les choses à tester ainsi que la façon de les tester peuvent varier énormément (et en conséquence le coût associé également).
    C’est pour cela que les « préconisation/recommandations » à l’emporte-pièce de David H. Hansson ne sont certainement pas à prendre au pied de la lettre, même si elles ne sont pas entièrement dénuées de bon sens et qu’il vaut le coup de s’y attarder et d’y réfléchir, en fonction de son propre contexte et avec un œil critique (dans le bon sens de ce terme).
    Comme le dit l’auteur du billet, le monde est nuancé et celui du test également !

  6. On n’est jamais trop prudent. Je suis seul codeur/testeur de mon appli et je préfère appliquer ce genre de tests aussi sur ma prod parce que justement, je suis sûr d’avoir « oublié » des comportements. Et non je n’utilise pas d’ORM mais juste la couche Zend_DB du Zend Framework. Elle me permet juste de faire des requêtes SQL sans taper de SQL à proprement parler.
    Et j’ai intégré cette exécution de tests unitaires un peu spécial à des tâches cron régulières pour éviter aussi le fait de spammer ma base. J’ai l’impression que ce n’est pas la meilleure façon de faire. Mais techniquement, je suis vite limité par mes connaissances sur le sujet. Ce n’est pas quelque chose qu’on apprend à l’école et j’en sors à peine (un peu plus de deux ans finalement)

  7. @Amaury : Dans ma conférence, je parle rapidement des tests unitaires. Je ne détaille pas beaucoup le concept parce que j’ai beaucoup de choses à évoquer et que c’est probablement l’une des choses les plus évidentes pour la plupart des gens aujourd’hui.

    @MathRobin : Concernant le test des accesseurs simples, je serais tenté de dire que s’ils sont si simples que cela les tests associés sont extrêmement simples, et donc rapides, à coder.

  8. Peut-être que la loi de pareto fonctionne 😉 50% de couverture, idenditie 80% des problèmes (vu qu’on la met à toutes les sauces)

  9. j’ai pas lu tous les commentaires, mais en ce moment je suis en train de regarder les screencasts d’un gars qui ecrit test avant code (et en fait, plus exactement, les deux simultanément)… Il a un ratio 1:1 je dirais… Je te l’enverrais à l’occas. et son code est couvert a 100%; et ses tests s’executent a une vitesse ahurrissante.

    Il a juste une méthode tellement rodée, tellement travaillée, que cela semble de la magie.

    (juste pour faire le contrepoint).

  10. Effectivement, il ne sert à rien de tout tester et le mieux et de garder en tête la fameuse formule « No risk, no test » que je commente au travers de mes pages:
    – Pas de risque, pas de test… Vrai ou Faux? (http://mind-testing.fr/?p=4)
    et
    – Stratégie de test, Approche de test (ISTQB) (http://mind-testing.fr/?p=113).

    Une stratégie de test s’élabore sur les risques et de bonnes références pour s’y référer sont:
    – TMMI (http://www.tmmifoundation.org/html/tmmiorg.html) certification
    et
    – TMAP (http://www.tmap.net/en) où il existe de nombreux templates et check-lists disponible gratuitement.

    Bonne lecture!

Laisser un commentaire

Votre adresse de messagerie ne sera pas publiée. Les champs obligatoires sont indiqués avec *

Notifiez-moi des commentaires à venir via email. Vous pouvez aussi vous abonner sans commenter.