TDD : refactoring de code Legacy (partie 1)

Dans un monde idéal, le développeur n’écrirait que du code neuf, qu’il pourrait rendre élégant et parfait. Ainsi, il ne serait pas confronté au code des autres, en général moche et incompréhensible. Il ne serait pas davantage confronté à son propre code plus ancien, qui en vieillissant ressemble de plus en plus à ce fameux code des autres.

La réalité est bien différente : il faut fréquemment revisiter son propre code, ou maintenir des projets vieux de plusieurs années. On y découvre des choses qu’on aurait souhaité ne jamais voir : du code dépassé, ignoblement compliqué ou encore du code spaghetti. En bref, du code Legacy.

Confronté à ce code Legacy, rien ne sert de geindre, d’en vouloir à la Terre entière, ou d’essayer de battre le record du nombre de WTF prononcés à la minute. Malgré toutes ces manifestations de désespoir, il faudra bien s’armer de courage et s’y mettre. Bon du coup, qu’est-ce qu’on attend ?

Qu’est-ce que du code Legacy ?

Il est difficile de définir précisément ce qu’est du code Legacy. Dans son ouvrage Working Effectively with Legacy Code, Michael Feathers en fournit une première définition assez lapidaire :

To me, legacy code is simply code without tests.

On trouve d’autres propriétés caractérisant du code Legacy, comme le fait qu’il soit incompréhensible, difficile à modifier ou à maintenir, ou encore qu’on dispose de peu de connaissances fonctionnelles et techniques à son sujet. Seulement, ce sont des critères subjectifs, difficilement évaluables.

L’absence de test est en revanche un fait concret, objectif. Et c’est une bonne indication de ce qu’on peut faire pour améliorer la situation : écrire les tests qui manquent.

Rendre le code testable : la technique du Golden Master

En voulant écrire des tests pour le code Legacy, on se retrouve très vite confronté à deux difficultés majeures :

  • il faut rendre le code testable : la façon dont il se présente, les dépendances avec les autres composants, etc. ne le rendent pas facilement testable.
  • il faut savoir comment tester le code : si on a perdu les spécifications et qu’on ne sait pas ce que le code est supposé faire, cette étape peut rapidement devenir inextricable.

La difficulté pour écrire des tests provient du fait qu’on ne sait pas contre quelles valeurs attendues comparer le code Legacy testé. On sait que ce code fonctionne en principe, sans savoir de quelle façon et avec quels résultats. Néanmoins, il faut considérer que si il existait des tests, ils fonctionneraient. On serait alors presque en situation de refactorer dans le vert. Presque, parce qu’avant de refactorer, il reste nécessaire d’écrire ces tests.

 

golden_master

Let the Golden Master teach us his technique.

C’est alors qu’intervient la technique dite du golden master. Elle consiste à considérer le comportement du code Legacy et les résultats des tests qu’il produirait comme un ensemble de données de référence. Ces données sont ensuite comparées aux résultats produits par la nouvelle implémentation issue du refactoring. Les résultats produits par la nouvelle implémentation doivent bien entendu être identiques aux résultats produits par le code Legacy. L’important ici n’est pas de savoir si le code Legacy a un comportement pertinent, mais de s’assurer que lors du refactoring on ne provoque pas de régressions.

Pendant le refactoring, on fait ainsi cohabiter deux bases de code : le code Legacy et le code en cours de refactoring. Si besoin, on est amené à modifier légèrement le code Legacy pour pouvoir le piloter par les tests et capturer les valeurs nécessaires aux tests.

Chaque fonctionnalité testée va être exécutée un nombre conséquent de fois, avec des paramètres d’entrée suffisamment différents. Les entrées peuvent être générées aléatoirement, l’important étant qu’on fournisse les mêmes séries de valeurs d’une exécution à l’autre.

Il existe différentes façon d’exploiter les résultats de référence :

  • à la volée : dans ce cas chaque résultat d’exécution du code Legacy est comparé directement au résultat de la même exécution pour le code refactoré.
  • après coup : les résultats de références sont enregistrés (dans des fichiers ou une base de donnée) pour chaque implémentation (legacy et refactorée). Les résultats enregistrés sont comparés après coup.

On dispose ainsi d’un filet de sécurité (test harness) relativement solide nous informant immédiatement lorsque le refactoring provoque une régression.

On peut alors refactorer en toute sécurité, avec la certitude qu’on avance dans la bonne direction. Etant en approche TDD, il faut procéder par petites étapes (baby steps), en avançant doucement mais sûrement et en évitant l’effet tunnel.

Au cours du refactoring, on va être capable de dégager une partie de la sémantique associée au code, ce qui ouvre la possibilité d’écrire de nouveaux tests indépendants du test harness et basés sur le comportement réellement attendu du code.

 

Dans un prochain article, nous donnerons davantage de détails pratiques au travers d’un kata de code approprié.

 

One thought on “TDD : refactoring de code Legacy (partie 1)

Laisser un commentaire