Ces derniers temps, j’ai eu plusieurs discussions avec d’autres informaticiens, orientées sur des sujets comme les méthodes agiles en général et plus particulièrement les tests unitaires. C’est toujours intéressant de confronter sa propre vision à celle des autres.
Une des choses qui m’a marqué, c’est que plus de la moitié des gens avec qui j’ai abordé le sujet avaient un raisonnement très binaire. C’est-à-dire que pour eux, 100% du code doit être couvert par des tests unitaires. C’est comme ça, ça ne doit pas être autrement. C’est simple et clair.
Sauf que… Cette vision des choses me gêne pour 3 raisons.
1. Le coût
Écrire des tests unitaires prend du temps. Cela a donc un coût. Tester l’intégralité du code oblige donc à y consacrer beaucoup de temps, ce qui augmente considérablement le budget en développement.
Le contre-argument habituel est de dire qu’en faisant appel au TDD (test driven development, dont je vous ai déjà parlé par le passé), ce coût est réduit. Ceci n’est pas complètement vrai : Le TDD force à commencer par écrire les tests, donc on est certains qu’ils existent. Au passage, on se creuse un peu la tête pour essayer d’imaginer les différents cas d’utilisation (légitimes ou générant des erreurs) dans lequel le code pourrait se retrouver. Mais le principe même des méthodes agiles, c’est de dire qu’on aura plus d’expérience demain ; donc il faudra forcément compléter ces tests au fil du temps. Ce qui ne pourra se faire qu’en cours de développement, voire même après les développements. Je connais beaucoup de développeurs qui utilisent la méthode TDD, et tous confessent devoir compléter leurs tests après coup.
Donc nous sommes d’accord, ça prend du temps, l’air de rien, et donc ça coûte cher. Il est inévitable de confronter ce coût à son utilité. C’est vrai, ça ; à quoi servent les tests unitaires ?
- Augmenter la stabilité des développements.
- Réduire les bugs de régression.
- Rendre le travail des développeurs plus confortable.
Le deuxième point découle du premier. Un code plus stable, qui se vérifie automatiquement, sera plus robuste face à des bugs de régression. Une fonctionnalité qui marche bien à un moment donné, mais qui se met à déconner à cause d’un effet de bord provoqué par un nouveau développement, ça arrive tous les jours. Les tests unitaires permettent de détecter cela au plus tôt et réduisent donc les risques.
Par contre, le troisième point me laisse un peu songeur. Je reste assez froid quand un informaticien me dit «Oui, mais avec 100% du code qui est testé, c’est beaucoup plus confortable pour nous !».
Certes, c’est vrai (encore que, voir plus bas). Mais sincèrement, même si je suis le premier à penser qu’une entreprise doit se soucier réellement du bien-être de ses employés, est-ce que le confort dont on parle vaut automatiquement le coût de l’écriture de tous ces tests ? Une entreprise a pour but de générer des bénéfices ; cela veut dire que chaque investissement doit être jugé à la lumière de ce qu’il va rapporter − ou de ce qu’il permettra de ne pas perdre (car si les tests unitaires ne font pas gagner d’argent, ils peuvent vous éviter d’en perdre beaucoup).
Tout ça pour dire qu’un développeur ne peut pas simplement dire que pour chaque heure de développement effectuée il lui faudra systématiquement une heure d’écriture de tests. Tout dépend du développement en question. En fait, c’est la même chose pour beaucoup d’autres points. Il y a des projets qui ne sont pas critiques pour l’entreprise, et pour lesquels on se permet tous les jours de faire l’impasse sur la qualité globale, la documentation, les tests ou autre chose. Tout simplement parce que l’important est de se confronter rapidement au marché, et que si le coût est élevé, le projet n’a alors plus lieu d’être.
2. La poudre aux yeux
Les tests unitaires ont toujours une part de “poudre aux yeux”. Ils peuvent donner un faux sentiment de sécurité. Le code est testé de manière automatique, alors on se repose sur la certitude que toute régression est impossible.
Oui, mais non. Il ne faut pas oublier que tester 100% du code ne veut pas dire que le code est testé à 100% (subtile nuance).
Le minimum est habituellement d’avoir au moins 2 tests pour chaque méthode : Un test pour le cas d’utilisation normal, et un test pour voir si le code est capable de gérer correctement les cas d’erreurs. Mais bien souvent cela n’est pas suffisant. Il faut tester différentes combinaisons de paramètres, vérifier les cas limites ou encore tous les cas d’erreur possibles.
Et même comme ça, vous n’êtes pas à l’abri d’avoir mal codé un test unitaire. C’est arrivé à un de mes amis, qui bosse dans une grosse boîte. Il y avait un petit bug dans son test unitaire, ce qui a laissé passer un autre petit bug dans son code. Le département QA (« quality assurance ») de l’entreprise a fait son boulot en exécutant les tests unitaires et en conduisant des tests d’intégration. Tout semblait fonctionner correctement. Mais une fois en production, ils sont évidemment tombés dans le cas limite très particulier qui a commencé à effacer des choses en base de données. En production.
Je ne suis pas en train de dire que les tests unitaires ne servent à rien. Pas du tout ! Mais il est vain de croire qu’on va pouvoir écrire des tests capables de vérifier toutes les possibilités sans la moindre erreur. Se reposer dessus aveuglément est une bêtise. Croire qu’on peut tout tester à fond sans générer de surcoûts est illusoire.
3. L’extrémisme de la fainéantise
Personnellement, j’évite d’avoir de grandes certitudes dans la vie. Je peux avoir des convictions, mais je suis prêt à les remettre en cause. Je trouve par contre très désagréables les gens qui sont absolument certains de connaître une des grandes vérités de la vie, qui s’y accrochent avec force, et qui ne laissent pas aux autres le droit de voir les choses autrement (quand ils ne tentent pas de les convaincre à tout prix).
Ça semble parfois être un sport pour les informaticiens, de dire que “leur” techno est la meilleure et que le reste ne vaut rien. Mais pour la peine, les extrémistes du test unitaire me font plutôt l’effet de fainéants.
- Il faut écrire des tests unitaires parce que c’est un confort incroyable pour les développeurs.
- Il est difficile de définir la «bonne» quantité de tests à écrire, alors il faut tout tout tout tester.
Ce qu’il y a de rassurant dans l’intégrisme, c’est qu’il évite d’avoir à se fatiguer ; il évite de réfléchir. Une fois qu’on a défini une règle stricte − irréfutable car totalement extrême et donc sans discussion possible − il suffit de l’appliquer. C’est simple, c’est facile, c’est apaisant.
Euh… je suis le seul à trouver ça bancal ?
La solution : le pragmatisme
J’essaye d’être quelqu’un de pragmatique. Quand il s’agit de mettre le curseur quelque part, je sais que même si la solution la plus simple est de le caler à un bout, la solution la mieux adaptée est souvent un peu plus nuancée. Et qu’avec la nuance vient la complication. La plupart du temps, il vaut mieux choisir la simplicité ; je sais aussi qu’il faut être capable d’accepter les nuances si elles sont nécessaires.
Dans le développement, les mêmes règles ne peuvent pas s’appliquer partout de la même manière. Tous les projets sont composés de briques logicielles. Certaines d’entre elles constituent les couches les plus basses et les plus stables dans les temps. D’autres forment la partie la plus éphémère, qui peut être amenée à être modifiée souvent et rapidement en fonction de l’évolution des besoins fonctionnels ou commerciaux.
Être «agile», cela veut dire qu’on est prêt à faire face au changement. Si on rechigne à modifier du code parce qu’on a investi du temps à en écrire les tests unitaires, et qu’on sait qu’il faudra de nouveau y passer du temps, c’est qu’on a tout faux. Si le coût du développement ajouté au coût de la création des tests aboutit à un projet trop cher pour valoir la peine, c’est qu’on a mal défini les priorités.
Je m’attache à stabiliser les couches qui doivent l’être. Sur du développement web, il s’agit habituellement des couches proches de la base de données, et le code métier. Les couches les plus proches du rendu graphique (contrôleurs, templates, … tout dépend de votre architecture technique) sont classiquement celles qui doivent pouvoir être modifiées “à la volée”. Les tests sont là pour limiter la prise de risque ; pas pour augmenter l’inertie des développements.
Je préfère faire l’impasse sur les tests d’un contrôleur qui a déjà été modifié quatre fois ces trois derniers mois, pour dégager plus de temps pour un refactoring qui sera bien plus bénéfique à long terme.
ça fait du bien d’entendre dire que sur les tests unitaires comme ailleurs, tout est dans le compromis.
comme tu dis, pas toutes les couches de code valent la peine d’être testées. Les couches les plus stables sur un site Web sont celles qui manipulent les données. Dans mes projets, la majorité de ces méthodes sont testées. Par contre quand on arrive au dessus, c’est plus délicat :
– le code serveur qui génère le HTML est rarement testé, sauf sur le chemin critique : signup, login et peut être coeur de métier comme affichage de produits
– le CSS n’est juste pas testable automatiquement. Encore qu’on peut bricoler ou utiliser un service qui prend des screenshots sur différents navigateurs, et regarder visuellement ce que ça donne, mais ça a tellement de limites …
– le javascript est testable certes, mais au même titre que le PHP : seules les couches de manipulation des données valent la peine d’être testées. Les interfaces changent trop vite pour que ça soit rentable, sauf peut être encore sur le chemin critique.
bref, c’est affaire de mesure mais je crois que le plus dur c’est d’évaluer le coût des bugs potentiels si les tests ne sont pas faits. D’expérience sur un très gros site avec beaucoup d’équipes qui ne communiquent pas très bien, j’avais évalué le temps de debug à 50% du temps de dev (ex: 2 semaines de dev = 1 semaine pour debuguer). Et le cout des bugs subis par l’utilisateur est lui incalculable.
Par contre le cout du TDD, c’est d’après moi autour du double de temps de dev
Très bon article, et très intéressant.
Pour moi, un code coverage de 100% reste un idéal jamais atteint, une asymptote vers laquelle on essaie de tendre mais sans être faisable.
J’avoue n’être jamais tombé sur un développeur qui peut affirmer que son code est couvert à 100%, mais plutôt le contraire avec des personnes ou des sociétés qui vont jusqu’à ignorer les tests !
En ce qui concerne le TDD (et BDD, d’ailleurs), là aussi la demie-mesure s’applique. Dans certains cas, c’est une approche très intéressante mais dans d’autres ce n’est soit pas applicable, soit inintéressant. Il faut l’utiliser avec parcimonie.
Très intéressant. Par contre, je trouve ton utilité des tests unitaires est un peu réduite :
– ils servent également de documentation / spécification, aussi bien à travers le nom des tests, que via le code des tests eux mêmes
– un code testé est souvent plus propre / mieux écrit
– les test sont un indice de fiabilité dans le choix d’une libraire open source
Je te rejoins sur le point du contexte, un code jetable n’est pas à tester. De même, le cout du TDD quand on ne sait pas trop où on va (au niveau de l’implémentation) est énorme.
Pour ce qui est de tester *tout* le code, tout dépend du niveau. Sur tout ce qui concerne le métier, je préfère le faire, et être sur que lorsque je suis à un niveau plus élevé (type vue pour une appli web), de pouvoir m’appuyer sur du code fiable (oui, c’est une forme de confort)
Par rapport à la question des changements, je me demande si les tests d’acceptation à très haut niveau (type Cucumber) ne sont pas une solution ?
@Michel : Oui, trop d’entreprises ne mettent pas en place de tests unitaires, ni d’intégration continue. Il en existe même qui n’utilisent pas de système de gestion de source (jamais confronté à ça personnellement, mais j’en connais). Mais les mentalités évoluent.
@tight : Je suis en partie d’accord avec tes remarques. Je n’ai jamais considéré les tests unitaires comme un remplaçant à la documentation. Les noms d’objets, de méthodes et de paramètres doivent être explicites, mais ce n’est pas suffisant. Avec juste quelques commentaires et un coup de JavaDoc/PHPDoc/Doxygen on peut faire de la documentation technique très utile pour un coût quasi-nul. Par contre, s’il faut lire le contenu des tests unitaire pour comprendre comme utiliser une librairie, autant lire son code source directement, ce sera plus simple.
Je n’ai jamais vu de réelle corrélation entre la présence de tests unitaires et la propreté du code. Tout dépend du développeur. S’il a de mauvaise habitudes, s’il code salement, il écrira ses tests comme un goret. Et il jugera inutile de s’appliquer dans l’écriture de son code, puisque les tests unitaires sont là pour jouer le rôle de garde-fou… (je forcis le trait à l’exagération, mais tu vois ce que je veux dire)
Nous sommes par contre tout à fait sur la même longueur d’onde quant au fait que le code qui se périme vite doit pouvoir se reposer sur des briques stables largement testées. Et il vaut mieux tester ces parties non pas par des tests unitaires, mais via des outils permettant de valider des scénarii utilisateur (mais là encore, il faut évaluer le coût).
Professionnellement, j’ai commencé par le développement, ensuite au réseau puis à l’administration d’un parc de serveurs/stations, maintenant que je retourne au développement j’ai changé d’idées sur tout.
Avant j’étais un intégriste des tests: Tout tester.
Maintenant c’est le minimum syndical, je me concentre sur les input (pour éviter les blagues style « \ »\ »;;;;;drop database; ») et la manipulation de données.
Pour le reste c’est le strict minimum et je rajoute beaucoup de log (c’est beaucoup plus utile).
En prod, la théorie du chaos c’est la normalité, quand une appli ne plante pas il n’y a qu’une raison:
personne l’utilise !!
Les tests unitaires ont tout testé ?? Sûr ??? Ils ont testé si l’Os plantait ? Si le processeur plantait ? Si le disque dur plantait (un secteur défectueux qui corrompt une base de donnée c’est super drôle…) ? Si une autre appli corrompait les fichiers de votre appli ?? Si l’antivirus effaçait des fichiers de l’appli ?? Si le serveur déconnait ?? Si le réseau déconnait ???
L’exemple du test unitaire qui laissé passé un bug qui effaçait la base de donnée, c’est un problème simple. J’ai connu plus « drôle », j’en ai 2 de mémoires:
Une appli communiquait entre plusieurs sites (de villes différentes), elle fonctionnait pas ou très très mal pour certains sites et très bien pour d’autres. Après 6 mois d’audit, « l’erreur » a été trouvée:
l’appli générait des requêtes tcp avec le flag no-frag, c’est à dire que la requête ne peut être découpée. Pour certains sites, la conf des matériels réseau intermédiaire (= ceux d’internet) faisait que les requêtes n’étaient pas découpée, mais pour d’autres si et la requête était rejetée à l’arrivée (c’est le réseau, c’est le chaos).
Second cas plus simple:
c’est une appli Java qui permet de scanner les documents, de les manipuler et le envoyer sur un serveur, etc …
Pour les scanneurs HP pas de problème, pour les scanneurs Canon non, quand l’utilisateur clique sur le bouton, une page web vierge apparait et rien ne se passe. Je test avec mon compte et ça marche.
Pourquoi ça merde ???
Java lance bien le contrôleur du scanneur, qui à son tour lance l’appli propriétaire pour scanner. Canon utilise une interface web avec des ActiveX pour scanner, à cause de la sécurité les ActiveX demandent la confirmation pour se lancer (l’utilisateur doit cliquer sur le pop-up pour accepter). Mais comme Java lance le scan via le contrôleur, l’utilisateur ne voit pas le pop-up, d’où la page vierge, et l’appli Canon attend la confirmation de l’utilisateur (qui ne peut pas la donner). Mon compte étant admin, les ActiveX sont automatiquement approuvés donc ça fonctionne.
Comment imaginer ça lors des tests ??? C’est ça la vrai source des bug, l’interaction avec le chaos.
Et je vous passe le coup où deux appli ont une conf contradictoire, quand on en installe une ça plante l’autre.
Les tests sont forcément fait dans un univers contrôlé, comme dans un labo.
Il faut faire un minimum de tests, protéger les données et générer une tonne de log. En prod le problème c’est pas qu’une appli plante, c’est qu’on ne puisse pas expliquer pourquoi. Si on sait pourquoi ça plante on peut s’en sortir. 😉
@Sylvain : Ton avis est intéressant. Je le nuancerais quand même un peu. Tout est une question de gestion des risques. Évidemment que les tests ne peuvent pas tout couvrir. C’est bien pour cela qu’il ne faut pas y passer « trop » de temps (par rapport à ce qu’ils apportent). Mais ils permettent un gain de temps non négligeable dans la détection des bugs de régression.
J’aime quand même ta remarque «quand une appli ne plante pas il n’y a qu’une raison: personne l’utilise !!». Je reformulerais de manière plus consensuelle en disant que les tests unitaires ne seront jamais une raison pour négliger les tests utilisateur en conditions réelle.
Un cahier de test, un recette en production, ça prend du temps, mais même avec du code sur-testé, on ne pourra jamais s’en passer !
Moi j’ai remarqué que ceux qui prônent tout tester ni croient pas eux-même , il s’agit de responsables qui pensent « si j’en demande 100% de manière autoritaire, j’aurai 50% et c’est déjà pas mal, çà me rassurera sur l’état de mon produit »
Un petit article sur les revues de spécifications, revues de code ferait une bonne suite 🙂
P.S : Ah.. mon rêve est d’avoir un vrai département QA dans ma boite 🙂
@Anon : Oui, ce genre de comportement peut arriver au sein d’une équipe, j’imagine. Mais il y a aussi l’effet «Faites ce que je dis, ne faites pas ce que je fais» qui est très répandu…
Pour information, la discussion continue sur le blog de Frédéric Hardy. On a un point de vue très différent sur la question lui et moi, je vous laisse lire son article, très intéressant au demeurant.
Prenez juste avec des pincettes les propos qu’il me fait porter. 😉
Cet article a connu une suite : Contre les extrémismes coûteux (le retour)
Bonjour, j’ai lu les deux articles, le votre et celui de Frédéric Hardy. (Je les ai découvert tardivement, je sais on est bientôt en 2018)
Je trouve des choses très intéressante dans les deux.
J’ai lu d’abord l’article de Frédéric Hardy et j’ai été surpris ensuite par le votre (après la lecture du premier je m’attendais au pire).
Je suis un fervent défenseur de TDD et pour autant en accord avec votre vision.
D’ailleurs, Kent Beck (co-créateur de JUnit et premier promoteur de TDD) a écrit sur stackoverflow :
« I get paid for code that works, not for tests, so my philosophy is to test as little as possible to reach a given level of confidence(…) »
https://stackoverflow.com/questions/153234/how-deep-are-your-unit-tests/153565#153565
Je trouve que la réponse de Frédéric Hardy est un peu diffamatoire, même si je suis également en accord avec beaucoup de points de son article (peut être une volonté de faire de la provocation et du buzz ?)
Je vous invite également à consulter ma vision sur les tests unitaires :
https://revuedecode.fr/post/voyons-petit-voyons-unitaire/
Bonne continuation
@François : Oui, j’avais d’ailleurs déjà repris cette citation de Kent Beck dans mon second article sur le sujet (http://www.geek-directeur-technique.com/2012/04/26/contre-les-extremismes-couteux-le-retour )
Concernant l’article de Frédéric (dont j’ai donné le lien dans un précédent commentaire), tu as pu voir les débats que nous avons eu lui et moi en commentaire. Je lui reproche évidemment son manque de diplomatie (quand il qualifie ce que j’écris de “bullshit”), son manque de nuance («Les tests, on y va à fond, ou on y va pas») et même sa manière de détourner les propos (cf. les commentaires sur son article, il modifiait l’article et les commentaires en fonction de ce qu’on se disait, changeait le sujet de la discussion en cours de route, etc.). Mais oui, il aime provoquer…
Pour résumer, mon propos est d’être pragmatique dans tout ce qu’on fait, qu’il s’agisse des tests unitaires ou d’autre chose. Toujours confronter les moyens mis en œuvre pour réaliser quelque chose à la valeur qu’aura cette chose une fois terminée.