MARGO

Actualité

Tous les mutants doivent mourir…

Retour sur la conférence “Unit testing implementation on Legacy code” de l’’Agile Testing Night

Par Taher Seifeddine Jegham Software Engineer

02/10/2018

C’est l’histoire d’un développeur appelé Léon…

 

Léon, plein d’énergie et de bonne volonté, cherche à écrire un code de qualité et à éviter les régressions. Il veut programmer des tests, refactorer les passages verbeux et supprimer les commentaires inutiles. Pourtant, Léon est face à une classe compliquée, incapable d’écrire quoi que ce soit, fixant son curseur qui continue de clignoter. La plupart du code a été développé bien avant que Léon ne rejoigne son équipe. Il en a lu une partie, sur laquelle il a travaillé, mais il n’a malheureusement pas une grande connaissance du reste.

 

Léon cherche désespérément de la documentation ou des tests à effectuer pour l’aider à mieux comprendre le rôle de la classe HahaTryAgainLoser, sans succès. Soudain, il reçoit une invitation à l’”Agile Testing Night”, organisée à La Société Générale à La Défense. C’est ainsi qu’il décide de se rendre à la conférence “Unit testing implementation on Legacy code” pour tenter de trouver une solution à son épineux problème.

 

Mardi 25 septembre : la montre de Léon indique 19h08, et l’événement a commencé il y a quelques minutes par une présentation dont il n’a pu entendre que quelques mots : agile, scrum, testing,… La keynote, appelée “Mutation testing” démarre finalement. Léon n’a aucune idée de ce qu’est un test de mutation, il écoute attentivement.

 

L’intervenant, Nicolas CHAZEAUX, commence par une affirmation : “la couverture du code est un KPI trompeur”. Pour démontrer ses propos, il écrit un scénario de test contenant une seule ligne appelant la méthode testée. La couverture est bien de 100% puisque tout le code de la méthode est exécuté. Cependant, le test en lui-même ne contient aucune assertion et ne défie aucune logique. Il est donc complètement inutile. L’écriture de tests unitaires définit quelques règles et comportements attendus que le code doit respecter. Ces règles dépendent généralement de la logique métier. Mais comment pouvons-nous être sûrs que toutes les conditions ont été testées ? C’est ici que les mutations entrent en jeu. Un mutant est une altération du code, par exemple un + qui devient un -, ou alors la modification d’une valeur constante. Considérons le code suivant :

 

Le scénario de test should_return_20_times_daily_salary devrait échouer et virer au rouge si le «*» est remplacé par un «/». Si ce n’est pas le cas, on considère que le mutant a survécu et que quelque chose nous a probablement échappé lors de l’écriture du test. Ainsi, nous n’avons pas respecté les spécifications métiers et nous avons altéré la règle cible. C’est ce qui se matérialise généralement sous la forme d’un problème de production ou d’un bug. L’intervenant a ensuite présenté quelques frameworks disponibles permettant les tests de mutation :

  • pour .NET: Stryker.Net
  • pour java: Pitest
  • pour Python: Mutmut.

 

Pour conclure sa keynote, Nicolas aborde les limites des tests de mutation, qui sont principalement :

  • leur lenteur d’exécution, ce qui les rend peu adaptés pour une utilisation à chaque modification dans le code
  • le fait qu’une analyse complète devienne compliquée à partir du moment où plusieurs mutants “survivent”.

 

Léon en sait désormais plus sur les tests de mutation, mais il ne sait toujours pas comment résoudre ses problèmes de code legacy…

 

Patrick Giry commence son intervention en rappelant la règle d’or : “ne modifie jamais le code, à moins d’avoir une demande importante ou un bug à résoudre”. Il explique que, bien que nous travaillons dans l’IT, nous nous devons de délivrer de la valeur pour le business, sinon, nous perdons notre temps. En changeant du code qui n’a rien à voir avec nos requêtes en cours, nous prenons le risque de causer des dommages, sans pour autant délivrer le changement attendu. Si vous voulez tout de même vous y risquer, il faut vous attendre à de mauvaises répercussions et surtout des utilisateurs mécontents face aux comportement étrange d’un composant qui n’aurait pas dû être impacté par la dernière version mise en production.

 

Pour améliorer le code legacy sans interférer avec la règle d’or énoncée plus haut, il y a deux moyens d’action :

  • le testing
  • le refactoring.

 

Pour le testing, supposons que nous travaillons sur du code sur lequel aucun test unitaire n’a été effectué. Il faut alors commencer les tests par la branche la moins profonde.

 

Considérons le code suivant :

 

Dans cet exemple, les branches les moins profondes sont les branches imbriquées “if”. Un bon point de départ est de spécifier le should_raise_MyError_when_not_condition_b () pour tester l’erreur de relance dans la branche “else”. Il en va de même pour le code à l’intérieur de la branche “if condition_b”. Cependant, on remarque qu’une autre classe de code est appelée. Cela rend le test unitaire plus compliqué puisqu’une logique extérieure est impliquée. En guise de solution, nous pouvons simuler ou redéfinir ce code afin de modifier son comportement dans le test unitaire et lui faire renvoyer la valeur que nous attendions. Cela aide à isoler la classe testée et évite de se laisser distraire par des problèmes externes. On peut alors répéter l’opération, isoler les branches, effectuer des tests, jusqu’à ce que le code soit couvert.

 

Pour le refactoring, il faut commencer avec la branche la plus profonde. Reprenons le code utilisé précédemment :

 

Dans cet exemple, la branche la plus profonde est la boucle “while” sur la condition_a. Il faut commencer par extraire la condition “while” dans une méthode en lui donnant décrivant une logique business. Cela nous mène à l’écran suivant :

 

Nous nous intéressons ensuite à la prochaine branche la plus profonde, qui est la branche “if” à l’intérieur du “while”, afin de l’extraire. A partir de là, nous pouvons continuer le refactoring en suivant le même process avec, à chaque fois, la branche suivante la plus profonde.

 

Il est maintenant 21h05, Léon se remémore tout ce qu’il a appris ce soir afin de la partager avec la communauté. Il retient notamment :

  • qu’il ne faut JAMAIS modifier le code, à moins de vouloir l’améliorer ou résoudre un bug
  • pour écrire des tests, il faut commencer par le branche la moins profonde
  • si une classe appelle une autre classe externe au code, on peut soit simuler, soit passer outre le comportement de la classe
  • pour effectuer un refactoring, il faut commencer par la branche la plus profonde
  • les tests de mutation servent à challenger sa logique de test
  • les tests de mutation sont plus lents que les tests unitaires, à la fois dans l’exécution et dans l’analyse.

Par Taher Seifeddine Jegham Software Engineer
Agile
Développement
Mutations
Refactoring
Testing