Article écrit par Ludovic de Luna
Ah le TDD ! Le sujet revient souvent sur le devant de la scène, dans des échanges informels ou des débats enflammés entre développeurs de tout poil. Et ses plus fervents défenseurs argumentent habituellement la chose de la sorte :
Dans MaBoite on fait du TDD ! Pour preuve, on a des tests unitaires, des tests d’intégration, des tests end-to-end… Notre couverture de test ? Oh, facilement 80-90% sur tous nos projets ! D’ailleurs c’est pas compliqué, c’est même écrit dans nos guidelines et nos offres d’emploi !
Seulement voilà, ici sont confondues deux pratiques : écrire des tests automatisés et guider l’écriture du code à l’aide de tests automatisés. Test Driven Development. Dans le premier cas, il est facile de court-circuiter l’étape d’écriture de tests (pour tout un tas de raisons, toutes plus ou moins pertinentes selon le contexte, toutes plus ou moins acceptables selon le locuteur) ; alors que dans les seconds cas, sans tests il n’y a pas de code.
Et le taux de couverture du code par les tests est un sujet à part entière, dont il faut retenir que cette valeur seule n’est qu’une valeur, elle n’est donc pas autorévélatrice, elle se doit d’être interprétée.
Je vous invite ainsi, cette semaine, à prendre la mesure de ce qu’est la pratique du TDD, quels sont ses enjeux et ses promesses.
La fausse bonne approche
Dans un monde fictif, un développeur fictif d’une entreprise fictive attaque une nouvelle feature fictive…
Alors on y va ! Devant l’écran, on a une vague idée du truc à coder, parfois avec des captures photo d’un tableau blanc abordé en réunion. Les puristes auront fait un joli schéma, mais le résultat sera le même. On démarre directement une classe et quelques méthodes dans les grandes lignes pour avoir un squelette de la logique recherchée. On voit pas trop le bout et on commence à griffonner des choses dans un cahier à côté.
Là, on se souvient qu’il faut écrire les tests avant d’avoir le code. Ah zut, je vais perdre du temps mais bon, il paraît que c’est une bonne pratique. Alors l’étape n°2 est d’écrire une série de tests, pour plus tard, qui échouent tous bien évidement. Ils sont pour la plupart de haut niveau, taillés pour coller au code déjà écrit et n’offrent aucune vision du besoin métier. Mais qu’importe. On se dit que c’est ça le TDD.
Au bout de quelques itérations de développement (plusieurs semaines ou mois), la lecture des tests devient compliquée et on finit par avoir des échecs énigmatiques. Dans le meilleur des cas, c’est aléatoire. Dans d’autres, c’est systématiquement les mêmes tests qui échouent mais personne n’a le courage de plonger dans le code pour fixer les tests. Des collègues agacés finiront par mettre en commentaire les tests en erreur.
Le plus bizarre reste quand même que, malgré les tests, les utilisateurs nous remontent des bugs… Mais ne faisons pas plus longtemps l’apologie de mauvaises pratiques.
À présent, j’ai une bonne et une mauvaise nouvelle.
La bonne nouvelle, c’est qu’un tourne vis est aussi utile à un électricien qu’à un plombier. Ici, le tourne vis, ce sont les tests automatisés. Et ce qui est bien, c’est que vous avez déjà l’outil.
La mauvaise nouvelle, c’est que si votre pratique ressemble à celle exposée ci-dessus, l’outil vous l’utilisez mal. L’approche décrite dans cet exemple fictif est nommée « test first », qui est un faux-ami du TDD. Elle donne une fausse impression de sécurité et n’apportera rien à la conception logicielle.
Alors, reprenons au début.
Test Driven Development
L’image qui me vient en tête pour ce principe, je la dois à Michaël Azerhad : le TDD, c’est comme un GPS. Il vous guide vers votre destination. Sans lui, vous risquez de longs moments d’errance et de parcourir inutilement un chemin tortueux… Dans l’hypothèse où vous trouvez au final la destination, évidement. Inutile de préciser qu’avec un peu de pratique, TDD fait vraiment gagner du temps.
TDD s’inscrit dans la mouvance de l’Extreme Programming et a été formalisé par Kent Beck dans son ouvrage « Test Driven Development : By Example ».
Son but est de découvrir une implémentation propre, répondant à un problème donné, par itérations successives. La méthode vise aussi bien la qualité du code que de réduire le stress sur le développeur en le sécurisant pendant la phase de développement. Les itérations visent à raisonner en découpant la complexité en unités plus petites.
TDD ne vous donne pas la solution au problème mais un cadre pour vous permettre d’atteindre la solution. Je le redis : lorsqu’on débute une session en TDD, nous ne connaissons pas la solution. Si c’est le cas, vous avez sûrement un biais qui pourra amener une complexité inutile. En revanche, on a une idée générale de la direction à prendre (mais aucun code pour valider cette direction).
Donc, avant de coder quoi que se soit, vous rangez le clavier et prenez conscience de la complexité de la tâche. Ensuite, vous pourrez tacler le problème en unités plus petites pour lesquelles vous pourrez vous projeter une représentation mentale et commencer votre route vers la solution.
Le cycle de TDD
Voici le déroulé :
- Écrire UN TEST et UN SEUL avant d’écrire le code correspondant. Le test que vous ajoutez doit valider une seule règle. Pour savoir quoi écrire, on s’appuie sur les cas remontés dans les « user stories » ou le test d’acceptation correspondant (idéalement sous forme de BDD). Il y a toujours une phase de réflexion avant pour savoir dans quelle direction on souhaite aller. Notre compréhension du problème se fera plus fine à chaque itération, nous aurons au fil de l’eau plus d’assurance sur notre direction. On peut commencer par une vision assez globale pour en comprendre la complexité et on se retient d’esquisser une solution prématurée.
- Jouer le test automatisé et constater qu’il échoue, principalement parce que l’objet ou la méthode n’existe pas encore. Si vous utilisez un langage compilé, la compilation échouera. Si c’est un langage interprété (Python, PHP, JavaScript, Ruby…), vous aurez une erreur vous indiquant l’absence d’implémentation. Le fait que le test échoue pour cette raison montre qu’on peut avoir confiance dans sa validation à terme. Pour ceux qui ont un doute, j’ai déjà vu des développeurs commencer à tester la mauvaise classe… Ce que le test permet de voir.
- Il faut écrire le code qui permet au test de réussir. On écrit uniquement le code nécessaire et rien d’autre. Pas de factorisation, aucune tentative pour rationaliser l’existant, ne tentez aucune abstraction, n’écrivez aucun code en amont d’un problème que vous n’avez pas encore rencontré. Il faut modifier ou écrire le code au plus simple… Et parfois le plus bête pour réussir le test. Dans l’idéal, écrire ou modifier le moins de chose possible.
- Rejouer les tests. En cas d’échec, jonglez entre le point 3 et 4 jusqu’à ce que les tests passent.
- Rationaliser le code : factoriser, renommer, restructurer et éventuellement ajouter une abstraction (voir les règles plus bas). Pensez aussi à rationaliser vos tests en supprimant celui qui répond exactement à la même question qu’un autre. Validez que les tests passent toujours à chaque modification en revenant au point 4.
- Répétez l’ensemble en démarrant au point 1, jusqu’à ce que la solution au problème soit implémentée.
Vous n’aurez peut-être pas la solution la plus optimisée, mais vous serez arrivé à destination. Rien ne vous empêche ensuite d’optimiser l’implémentation.
Lorsqu’on quitte une session de développement, on se laisse un test en rouge pour savoir où reprendre à notre retour. À l’inverse, lorsqu’on partage un développement (pour une revue ou autre), tous les tests doivent être au vert.
Les règles à suivre
Kent Beck donne 2 règles pour pratiquer :
- Ne pas écrire de code avant d’avoir un test en échec.
- Rationaliser le code en éliminant la duplication de responsabilité ou de logique (phase de factorisation).
Pour ce second point, ne cherchez pas forcément des lignes identiques ou qui se rapprochent. L’intention est bien plus importante que la forme (je vous ramène à notre article sur DRY : duplication ou coïncidence). Et n’oubliez pas d’appliquer cette logique aux tests eux-mêmes, mais ne modifiez pas tout en même temps : soit vous modifiez les tests, soit vous modifiez le code — mais pas les deux en même temps.
Quelques années après, Oncle Bob (Robert C. Martins) a étendu la vision d’origine avec ses « 3 lois de TDD » qui concernent surtout la règle #1 de Kent Beck :
- Vous n’êtes pas autorisé à écrire un code sans le test en échec correspondant.
- Vous n’êtes pas autorisé à écrire plus d’un test en échec à la fois.
- Vous n’êtes pas autorisé à écrire plus de code que nécessaire à la réussite du test.
On retrouve ici l’essence même des règles de Kent Beck, mais la vision d’oncle Bob va un peu plus loin en nous imposant d’avancer étape par étape. Cette façon de progresser est essentielle. Si on brûle les étapes, on perd le cap et la durée du développement s’allonge.
Ce qui sous-entend d’écrire du code simple — parfois idiot — pour répondre à un test. Ceci veut dire que l’implémentation n’est pas toujours correcte, mais vous devez avancer ensuite avec d’autres tests. Tenez-vous les 10 doigts pour ne pas enchaîner un if
imbriqué si vous n’avez pas commencé par passer un test via un if
/ else
simple. Inutile de mentionner que si vous débutez votre code avec un case
, vous avancez trop vite.
Évitez également de créer des abstractions en phase d’écriture d’un code pour répondre à un test. On ne voit l’évidence d’une abstraction qu’en phase de factorisation et à condition d’avoir réellement deux éléments (à minima) qui auraient mérité une abstraction. Si vous n’êtes pas certain, laissez en l’état pour avancer. Vous aurez largement le temps de vérifier vos hypothèses aux prochaines phases de factorisation et vous évitez une abstraction inutile qui vas compliquer vos prochaines étapes.
Conclusion
Il y a bien d’autres sujets autour du TDD, et nous n’avons vu que l’introduction. Si ce sujet vous passionne (et il le devrait), je vous recommande la lecture de l’ouvrage d’origine de Kent Beck. TDD vous permet à la fois de progresser sereinement et avec le bon rythme.