“Sur un projet tech existant, refactoriser sans tests, c’est okay ?”
La réponse évidente serait de répondre un “non” catégorique.
Le raisonnement se base sur l’une des raisons d’être des tests : sécuriser la solution face aux régressions.
Agir sur un code sans tests, c’est littéralement marcher dans un champ de mines.
Tout changement, aussi minime soit-il, peut introduire des bugs et des régressions sur l’existant sans prévenir.
L’image est facile à comprendre. Pas besoin d’en dire beaucoup plus.
Dans n’importe quel contexte normal, le sujet s’arrêterait ici. Tout serait parfait. Super !
Sauf que…#
Sauf qu’on traite ici de legacy. Et malheureusement, legacy n’est pas synonyme de belle prairie avec un champ de fleurs à perte de vue, mais bien de ce champ de mines mentionné précédemment, avec en prime les barbelés pour la déco.
La situation n’est clairement pas aussi simple et limpide que le contexte idéal qu’on aimerait avoir.
Dans notre contexte, si on s’arrête au concept de base “on ne modifie jamais du code sans tests”, à moins d’avoir les moyens financiers et opérationnels à la hauteur de ces exigences et ambitions, cela va devenir extrêmement difficile d’espérer pouvoir avancer et débloquer la situation.
Je veux que ce soit sans risques, pas cher, et rapide.
— La désillusion
Pour mieux le comprendre, voici une mise en situation :
Mise en situation#
Dans ce scénario, nous avons une solution legacy massive avec une couverture de tests faible à la limite de l’inexistant, comportant un nombre conséquent de tests obsolètes ou inefficaces.
L’équipe est constituée de quelques développeurs et d’un budget limité.
Le métier reste disponible, mais avec un temps limité à nous consacrer.
Donc tout d’abord, les méthodes traditionnelles :
Option 1 : Partir dans l’optique qu’il faut réaliser des tests classiques avant d’agir. Malheureusement, en l’état le code n’est pas testable. Il n’a pas été conçu avec cela à l’esprit.
Option 2 : Faire un strangler fig pattern (faire cohabiter une nouvelle solution avec l’ancienne et remplacer progressivement le legacy). Sauf que cela est en soi un nouveau projet à part entière, nécessitant du budget dédié et un engagement sur la durée.
Option 3 : Faire des tests de caractérisation (réaliser des tests via des captures du comportement existant). Cela aurait probablement été la meilleure solution ici. Malheureusement cette approche est, elle aussi, un investissement important à faire pour pouvoir identifier les comportements que l’on souhaite capturer avec l’aide du métier et ainsi construire un jeu de données représentatif.
3 options, mais aucune qui ne convienne vraiment avec notre scénario initial…
C’est foutu ?#
Si on reste sur cette idée de ne vouloir prendre aucun risque et d’attendre un alignement parfait des astres, voire peut-être d’un miracle, alors oui, c’est probablement foutu. Le projet est condamné.
Cependant…
Si on commence à réfléchir en termes de compromis… Il nous reste des cartes à jouer.
On pourrait très bien avancer par mini-itération ciblée avec un commit à chaque avancée (commit atomique), en commençant toujours par le plus facile et moins risqué (renommage automatique via IDE, découpage du code, supprimer le code mort, etc.) et en compilant / testant manuellement régulièrement pour s’assurer que cela fonctionne toujours.
Une fois le code modifié, en le rendant au passage cette fois testable, on pourrait maintenant faire des tests pour verrouiller la progression face au legacy.
En réalisant ceci, nous passons d’une situation où la dette technique avait la mainmise sur le projet et dont aucune action n’était envisageable, à une situation où nous pouvons maintenant progressivement améliorer le code en acceptant un risque maîtrisé et proportionné.
C’est quand même un peu plus encourageant non ?
La gestion du risque#
Le risque est une variable, au même titre que le coût, le temps, et la qualité. Tout est une question de compromis.
Dans cette situation de compromis à faire, il reste bien évidemment important de prendre en considération la criticité des sections qui constituent le legacy. On ne va pas appliquer le même niveau de risque entre le service de paiement et un autre module très secondaire.
La stratégie s’adapte selon le contexte et n’a aucunement besoin d’être appliquée uniformément sur l’ensemble du projet.
Et oui, comme vous l’aurez compris, même en prenant toutes les garanties du monde, il est possible que nous introduisions quand même un bug.
Mais prenez cependant également en considération le scénario où nous aurions décidé de ne rien faire : nous aurions simplement choisi de ne pas prendre de risque aujourd’hui, au prix d’un risque futur plus grand.
Ne pas prendre de décision, en legacy, c’est tout de même prendre une décision, celle de laisser la dette technique grossir.
L’inaction et l’immobilisme ont un coût réel qui se paiera le moment venu. Prendre conscience qu’on peut agir en gérant le risque, c’est déjà faire un grand pas vers la reprise en main de la situation.