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.