Le double cycle TDD : ne sous-estimez plus l'étape de refactoring

Préambule

attention

Cet article ne traite pas de la pratique d’ATDD (acceptance Test-Driven Development) et de sa “double loop of TDD”.

Il se concentre uniquement sur la pratique de TDD (Test-Driven Development) et ses étapes internes.

Le cycle TDD

La problématique

Les avantages du TDD ne sont plus à prouver. Cependant, l’étape la plus sous-estimée est le refactoring.

En français, ce terme désignerait l’amélioration, le réusinage, la restructuration ou le remaniement du code.

Qui omet cette étape ne fait pas de TDD !

attention

Remarque : bien évidemment, la partie refactoring n’est pas couplée à TDD, et il est possible de faire du refactoring sans TDD, par contre, TDD est fortement couplé au refactoring : il n’est donc pas possible de faire du TDD sans refactoring.

Qu'est-ce que le refactoring ?

Cette activité consiste à modifier le code pour gagner en qualité, lisibilité, design, maintenabilité et évolutivité, mais sans en modifier son comportement.

Pourtant, cette étape de refactoring est parfois oubliée, négligée, repoussée à plus tard (sauf que la loi de LeBlanc dit “plus tard = jamais”).

Les trois étapes

Si cette étape de refactoring est bien présente, il est à noter que son temps d’investissement est souvent plus important que celui des autres étapes.

Sans doute que le schéma classique TDD mène à cela, car les 3 étapes (souvent nommées “red”, “green”, “refactor”, traduites par “rouge”, “vert”, “amélioration”) ont l’air d’être complètement  équivalentes :

Le cycle TDD : red, green, refactoring

Le cycle commence toujours par l’écriture d’un test, qui échoue, et en l’occurrence qui apparaît “rouge” dans notre outil de développement.

Il est suivi de l’écriture du minimum de code, permettant juste de le faire passer au vert (c’est-à-dire que le test est maintenant fonctionnel au travers du code qui vient juste d’être écrit).

La dernière étape, elle, peut se révéler bien plus longue, bien plus complexe et demande beaucoup plus de compétences que les deux premières.

Popularisation du schéma

A noter que ce schéma n’a jamais été publié par Kent Beck. En effet, dans son livre originel, il décrit la pratique de TDD en cinq phases :

  1. écriture d’un test ;

  2. le faire compiler ;

  3. l’exécuter et le voir échouer ;

  4. le faire fonctionner ;

  5. supprimer la duplication.

A priori, la factorisation en trois étapes est l'œuvre de James Shore, qui considère que l’écriture du test et son échec font partie de la même étape. De même, le faire compiler et fonctionner sont une seule étape.

Quant à la suppression de la duplication de Kent Beck, même s’il explique “make it run, make it right” (faites qu’il s'exécute, faites le bien), il est un peu réducteur de se concentrer uniquement sur la duplication. En effet, l’activité d’amélioration est beaucoup plus large que cela, d’où le terme “refactoring”.

James Shore ajoute une notion de répétition et Kent Beck, de cycle, d’où l’émergence du fameux schéma.

Ce cycle de trois étapes a été largement adopté par la communauté des développeurs, adepte de TDD (Robert C. Martin, Martin Fowler, Ron Jeffries, etc.).

Toutefois, si nous nous basons sur ces trois étapes, faut-il mettre sur le même plan les étapes “red”, “green” et “refactoring” ?

Un double cycle TDD ?

Premièrement, cette étape d’amélioration n’est pas une étape courte comme les deux autres. Elle ne consiste pas à juste apposer un peu de code, en une seule fois, et de passer à l’étape suivante (sauf, peut-être, avec les premiers tests). Elle est, en fait, un cycle à part entière, où toutes ses étapes internes permettent d’améliorer le code, de façon continue.

Finalement, il n’y aurait pas qu’un seul cycle TDD, mais un double cycle, un cycle principal et un cycle d’amélioration, ce dernier étant la 3ème étape du cycle principal :

Double cycle TDD : cycle principal et cycle d'amélioration

Dans ce deuxième cycle, il n’y a pas de nombre maximum d'étapes. Il faut itérer tant qu’il est toujours possible d’améliorer. C’est donc une répétition de l’étape 3 jusqu’à ce qu’il ne soit plus possible d’améliorer les choses.

Attention, parfois il ne faut pas trop anticiper les améliorations, sous peine de revenir en arrière après avoir ajouté un nouveau test de comportement (qui peut être comblé en se mettant des limites de temps, au moins au départ).

Pourquoi le cycle de refactoring est capital ?

Concernant ces trois étapes :

  • l’étape “rouge” est importante car elle permet d’amener une spécification de ce qui est attendu. C’est un premier choix de design, car il faut définir quoi appeler, avec quelles données, et quelles sont les interactions. La difficulté est qu’elle demande un recul sur l’attendu et certaines personnes sont déstabilisés par ce mode de pensée inversée ;

  • l’étape “verte” est celle où la réflexion est la moins présente, car il faut aller au plus vite pour obtenir un test qui devient opérationnel (passage du rouge au vert). Certains passent trop de temps sur cette étape, sûrement le fait de ne pas faire assez de petits pas et il faut alors s’améliorer dans la discipline ;

  • l’étape de refactoring est une activité importante, car c’est là où la réflexion est omniprésente, tout en étant serein sur le code produit. En effet, nous réfléchissons à améliorer le code et sommes protégés par des tests qui nous alertent lorsque nous avons cassé le comportement. C’est là que nous faisons des choix internes cruciaux pour le design (en tout cas pour un “classicist”, c’est-à-dire qui épouse la méthode telle qu’elle a été initialement proposée par Kent Beck, en opposition au “mockist”). Sans parler des travaux sur la lisibilité, la maintenance et l’évolutivité.

Comme évoqué précédemment, les personnes qui ne font pas de refactoring, ou bien un refactoring très léger, ne font pas de TDD, car le cycle est incomplet.

Ceux qui ne maîtrisent pas les techniques de refactoring avancées font finalement du TDD partiel et l’absence de certains aspects (lisibilité, design, maintenance, évolutivité, etc.) posera des problèmes par la suite.

Pour mettre plus en lumière le refactoring, nous pourrions citer une des valeurs du manifeste craftsmanship : “Pas seulement des logiciels opérationnels, mais également des logiciels bien conçus”.

L’étape verte s’assure que le logiciel est opérationnel et c’est donc l’étape de refactoring qui permet sa bonne conception.

Enfin, ce cycle d’amélioration ne concerne pas uniquement le code qui part en production, mais concerne également le code du test lui-même. Même si les techniques peuvent être différentes, l’objectif est le même : amélioration du code quel qu’il soit.

Ce cycle d’amélioration est donc capital et demande une attention particulière.

La revue de code ou le pair programming sont de bons leviers permettant de s’améliorer sur cet aspect.

Evidemment, TDD est fait de multiples itérations du cycle principal, permettant une production incrémentale, qui permet d’obtenir du code :

  • répondant à la demande ;

  • fonctionnellement optimisé ;

  • simple (et non simpliste) ;

  • testable (et testé) ;

  • qui admet une documentation synchronisée des comportements en place.

Comme le principe de TDD c’est d’avancer par petit pas, le refactoring en est également facilité car basé sur une base de code réduite.

Que faire pendant le cycle de refactoring ?

Présentation

Nous allons vous proposer, ici, quelques éléments sur lesquels être attentifs, ainsi que quelques techniques de remaniement de code lors de la pratique du TDD.

Le cycle de refactoring permet de travailler plusieurs aspects :

  • la lisibilité ;

  • l’algorithme ;

  • le design ;

  • les tests.

attention

Remarque : l’amélioration sur une large base de code déjà présente, avec des tests souvent absents, qu’on nomme “code legacy”, demande des compétences de refactoring supplémentaires que celles présentées ci-dessous.

De plus, les bonnes pratiques suivantes ne sont, ni exhaustives, ni à suivre à la lettre car à contextualiser suivant les cas à traiter.

Travailler la lisibilité

Pour travailler la lisibilité, nous explorons les éléments suivants :

  • le nommage, les variables, les constantes, les classes, méthodes et fonctions, renforcés par l’application des règles des objets dits Calisthenics :

    • un seul niveau d’indentation par méthode ;

    • limiter l’instruction “else” en faisant des retours au plus tôt (principe de “fail fast”) ;

    • encapsuler les types primitifs dans des objets (souvent décrit comme “primitive obsession” et fortement repris dans Domain-Driven Design ou DDD) ;

    • respecter la loi de Demeter, autorisant à ne parler qu’à ses amis immédiats et donc de ne pas exposer sa structure interne à l'extérieur ;

    • ne pas utiliser d’abréviation, n’étant pas toujours très compréhensibles par tous ;

    • garder un nombre de lignes relativement succincts, en appliquant le principe de “extract ‘till you drop” ;

    • garder un nombre de variable d’instance limité ;

    • ne pas ajouter de “setter”, ni de “getter” systématiquement, afin d’encapsuler les traitements et qui consiste à appliquer le principe de “tell, don’t ask” ;

  • d’autres éléments basés sur le code final que nous souhaitons laisser une fois terminé, comme :

    • la suppression du code dupliqué sur une même intention (DRY, acronyme de “Don’t Repeat Yourself”) ;

    • le suivi des conventions d’équipe pour plus d’homogénéité.

Travailler l'algorithme

Pour améliorer son algorithme, nous serons attentifs aux points suivants :

  • ne pas écrire du code qui n’a pas été dicté par un test ;

  • simplification d’un traitement compliqué ;

  • limiter la complexité des méthodes et des classes ;

  • s’assurer qu’à partir de multiples instructions simplistes, nous voyons émerger un algorithme générique (souvent via la triangulation) ;

  • veiller à ne pas avoir de complexité accidentelle (notion introduite par Fred Brooks), c’est-à-dire la présence de complexité en raison de choix non pertinents ;

  • ne pas introduire de code défensif inutile ;

  • utilisation de la syntaxe du langage la plus adéquate ou des nouveautés plutôt que d’alourdir le code avec des anciennes syntaxe ;

  • restructurer le code en vue du prochain test : ajouter un test et s’apercevoir que la structure actuelle ne permet pas de le faire passer rapidement au vert. Il faut alors désactiver ce test, restructurer le code existant afin de se mettre en position d’accepter le futur code ;

  • travailler le design.

Travailler le design

Le design doit être maintenable dans le temps, il est indispensable de travailler :

  • la mise en place des architectures logicielles adaptées au besoin (en couches, Hexagonale/Clean architecture, CQRS, event sourcing, etc.) ;

  • la modélisation métier où les patterns tactiques DDD nous apportent des solutions et permettent d’avoir une modélisation riche (données et comportement au même endroit) :

    • entity ;

    • value object ;

    • agrégat ;

    • etc.

  • les principes de design, comme les principes S.O.L.I.D. :

    • nous déplaçons des instructions dans des emplacements plus adéquats (“responsabilité unique” / “Single responsability”) ;

    • nous rendons le code plus évolutif avec le principe “Ouvert-fermé” (sans tomber dans le mode “au cas où” mis en évidence par le principe YAGNI : “You Ain’t Gonna Need It”) ;

    • nous n'altérons pas le contrat pour une instance spécifique, comme évoquée dans le principe de “substitution de Liskov” ;

    • nous découpons naturellement nos abstractions, comme nous pousse à le faire le principe “d’Interface ségrégation” ;

    • nous rendons le code plus maintenable, plus flexible, notamment avec le principe “d’inversion de Dépendance”.

  • les caractéristiques/propriétés C.U.P.I.D. :

    • nous voulons une architecture logicielle avec des éléments qui s’assemblent bien ensemble, telle que précisée par la propriété “Composable” ;

    • nous nous assurons, avec la propriété “philosophie Unix”, que chaque élément ne doit faire qu’une chose et le faire bien ;

    • nous devons rendre le code “Prévisible”, afin d’être maintenable, observable, d’éviter certaines anomalies, mais également d’être réutilisable ;

    • nous voulons rendre le code naturellement compréhensible par tous, en appliquant la propriété “Idiomatique” ;

    • nous devons nous assurer que la solution est “basée sur le Domaine”, c’est-à-dire qu’elle épouse le métier. Nous avons, ici, une porte ouverte vers DDD (Domain-Driven Design).

  • les design patterns éprouvés :

Travailler les tests

Les tests étant également du code, il faut :

  • en prendre soin ;

  • que le nommage soit une véritable documentation des comportements ;

  • qu’ils soient bien structurés (souvent en 3 parties “Given / When / Then” ou “Arrange / Act / Assert”) ;

  • qu’ils soient faciles à écrire :

    • améliorer la productivité d’écriture des tests en créant une “factory” ou un “builder” pour générer des jeux d’essai rapidement, à partir du code écrit dans les premiers tests ;

    • posséder des doublures de test facile à écrire et à utiliser (sans forcément passer par des librairies, car elles allongent le temps d’exécution, elles suppriment un savoir-faire en imposant alors une compétence d’outil, pouvant même masquer un problème de design) ;

  • etc.

Bilan des travaux de refactoring

Beaucoup d’aspects sont à prendre en compte pendant l’étape de refactoring et les compétences pour maîtriser les techniques sont nombreuses.

Certaines pratiques de refactoring se chevauchent, mais cela souligne sans doute le fait qu’elles soient importantes.

L’outil de développement est également un très bon allié, il peut proposer des améliorations potentielles qu’il faut analyser avec attention.

Conclusion

Au sein du cycle principal TDD, l’étape de refactoring est donc un deuxième véritable cycle à part entière et c’est ce cycle qui est responsable des principaux avantages du TDD.

Cette étape peut donc être assez longue mais c’est l’étape la plus intéressante, celle où nous donnons du sens à notre code, mais également celle qui demande le plus de compétences pointues.

Références

Quelques références sur des concepts abordés dans l’article, mais qui peuvent également aider à s’améliorer dans la pratique de TDD :

  • le livre “Test-Driven Development by example” de Kent Beck pour la référence initiale à TDD ;

  • l’article “Red-Green-Refactor” de James Shore, pour la popularisation des étapes qui conduiront au célèbre schéma ;

  • l'article "Le R.O.I. du TDD" qui aborde le retour sur investissement du TDD par Ludovic Cinquin (CEO d'Octo Technology au moment de la publication de son article) ;

  • le livre “Refactoring” de Martin Fowler ;

  • le discours “TDD, Where Did It All Go Wrong” de Ian Cooper à la conférence DevTernity 2017 qui décrit pourquoi certains échouent à faire du TDD et met l’accent sur l’architecture hexagonale ;

  • le discours “Architecture Hexagonale & Clean architecture bonnet blanc, blanc bonnet ?” de, moi-même, Christophe Breheret-Girardin, lors du Comptoir Octo annonçant la Duck Conf 2023 ;

  • le discours “Does TDD Really Lead to Good Design ?” de Sandro Mancuso à la conférence DevTernity 2019 pour la différence entre “classicist” et “mockist” ;

  • le livre blanc Octo “Culture code” par Abel André, Arnaud Huon, Cédric Rup, Christophe Thibaut, Julien Jakubowski, Julien Tellier, Michel Domenjoud et Nelson Da Costa ;

  • le livre “Clean code” de Robert C. Martin ;

  • l’article “CUPID - for joyful coding” de Dan North ;

  • le livre ”The Thoughtworks Anthology” de Jeff Bay, Martin Fowler, Rebecca Parsons, Roy Singham, Michael Robinson et Neal Ford, pour la référence initiale aux objets Calisthenics ou l’article du même Jeff Bay ;

  • l’article “Assuring good style for object-oriented programs” de Karl J. Lieberherr et Ian Holland, pour la référence initiale à la loi de Demeter ;

  • l’article “The Art of Enbugging” de Andy Hunt et Dave Thomas pour la référence initiale au principe de “Tell, don’t ask” ou l’article de Martin Fowler ;

  • le livre “The Pragmatic Programmer” d’Andrew Hunt et David Thomas pour la popularisation de l’acronyme DRY ;

  • le livre “The UNIX philosophy” de Mike Gancarz ou l’article du MIT ;

  • Ie livre “Implementing Domain-Driven Design” de Vaughn Vernon ;

  • le livre “Design Patterns : Elements of Reusable Object-Oriented Software” d’Erich Gamma, Richard Helm, Ralph Johnson et John Vlissides ou l’article de JavaTpoint;

  • l’article “No Silver Bullet - Essence and accident in software Engineering” de Fred Brooks pour la référence initiale à la complexité accidentelle, ou son livre “The mythical Man-Month” (dans son édition anniversaire).