Aujourd’hui, la qualité du code est un sujet qui s’invite régulièrement au sein des équipes de développement. Ainsi pour améliorer la qualité d’un projet, naturellement on s’oriente vers des tests unitaires (TU) pour vérifier qu’une portion de code fonctionne bien.
Un des principes phares de la méthode de développement agile Extreme Programming (XP) est le Test first. Il s’agit d’écrire son test avant même d’avoir écrit son code. Cette méthode de développement est aussi connue sous le nom de Test Driven Development (TDD ou les tests pilotés par le développement en français).
Le dernier indicateur pour vérifier que l’application est bien testée est le code coverage (taux de couverture du code par les tests en français). Mais est-ce réellement suffisant pour vérifier la qualité de ses tests ?
Les limites de la couverture des tests
Le code coverage est une métrique importante, mais s’il y a bien une chose qu’elle ne vérifie pas, c’est la qualité des tests, pire elle peut être trompeuse. Elle ne vous montre que le code que vous avez exécuté, pas le code que vous avez vérifié.
La couverture de code est un guide, pas un objectif. Elle vous aide à écrire les bons tests pour valider un des chemins d’exécution de votre code.
La qualité des tests que vous écrivez dépend de la compétence et de l’attention que vous portez à leur rédaction. La couverture a peu de pouvoir pour détecter les tests accidentellement ou délibérément bâclés.
C’est là qu’entre en jeu, le mutation testing. Pour faire simple, c’est une méthode qui vérifie la robustesse de nos tests unitaires via l’utilisation d’une librairie dédiée, dans notre cas, Pitest.
C’est quoi un test de mutation?
Les tests de mutation consistent à créer des copies défectueuses de votre code et à analyser les résultats de l’exécution de la suite de tests par rapport à ces copies. Si un test échoue, on dit que le mutant est tué. Si aucun test n’échoue, on dit que le mutant a survécu.
Une mutation de code est un changement mineur qui affecte le comportement général de ce code. Voici quelques exemples : la suppression d’une ou plusieurs lignes de code, le remplacement d’opérateurs arithmétiques (par exemple remplacer une addition par une soustraction, le remplacement des opérateurs logiques (par exemple remplacer un « < » par un « <= ») ou le remplacement du retour d’une méthode par un objet null ou vide.
L’objectif est de mettre à jour votre suite de tests afin de tuer tous les mutants en faisant échouer tous les tests.
En savoir plus : https://pitest.org/quickstart/mutators/
Pitest est un outil de test de mutation pour Java qui possède une bonne intégration avec les environnements de développement intégrés tels qu’Eclipse ou IntelliJ. Ainsi, que les outils d’analyse statique du code tel que SonarQube.
Pitest génère des mutants en manipulant le bytecode. Cette approche offre des avantages significatifs en termes de performances par rapport aux fichiers mutants compilés, mais présente quelques inconvénients. Parfois il se peut que la mutation ne corresponde pas à un changement que le développeur pourrait réellement faire.
Une fois les mutants générés et les tests unitaires exécutés, Pitest fournit un rapport clair de l’exécution des tests, qui facilite la navigation entre le code source et les mutants et met en évidence les mutants qui n’ont pas été tués
Ça donne quoi Pitest en pratique ?
Dans mon projet Java, je vais implémenter une classe FizzBuzz (implémentation du jeu du même nom, pour enfants leur apprenant la division de manière ludique. Le jeu consiste à compter de 1 à 100 en remplaçant les nombres multiples de 15 par “FizzBuzz”, les nombres multiples de 3 par “Fizz” et les nombres multiples de 5 par “Buzz”).
Imaginons que mon chef de projet me demande de mettre en place en urgence des tests unitaires et d’avoir absolument 100% de couverture de tests.
Je vérifie le code coverage et j’obtiens bien 100%.
Le chef de projet est très satisfait. Mais est-ce réellement une garantie de la qualité du code ? Un développeur expérimenté remarquera que mes tests ne sont pas satisfaisants pour valider le bon fonctionnement de mon application.
C’est maintenant que Pitest entre en jeu. Pour commencer, je vais importer la librairie dans la configuration Maven.
et ensuite, exécuter la commande suivante :
Pitest va générer un rapport au format html dans le dossier target/pit-reports, voici un extrait :
Nous pouvons voir que dix mutants ont été générés et seulement 3 sur 10 ont mis en échec les tests unitaires (validant ainsi leur bon fonctionnement). Pour plus de détails, naviguer dans le package et sélectionner une classe Java.
Maintenant, la question que je dois me poser est “ Pourquoi ces mutants ont-ils survécu ? ”. Le rapport montre que parmi les dix mutants générés, nous avons deux types de mutator : changement d’opérateurs (multiplication au lieu de modulo) et changement du retour d’une méthode par une chaîne de caractères vides.
En regardant de plus près les tests unitaires, je peux remarquer que j’ai fait plusieurs erreurs de programmation.
Premièrement, pour la fonctionnalité où les multiples de 3 retournent bien “Fizz”, je vois que je ne vérifie pas le retour de la méthode getResult. Je fais juste “ Assert.assert True(true)”. Le mutation testing va modifier le retour par une chaîne vide ou les opérateurs. Les mutants ne vont pas être tués par les tests.
Deuxièmement, pour la fonctionnalité où les multiples de 5 retournent bien “Buzz”, là je ne vérifie absolument pas si le retour de la méthode est valide. Donc pareil que le premier point, les mutants ne vont pas être tués par les tests.
Dernier point, nous pouvons voir que pour la toute dernière fonctionnalité de FizzBuzz où les chiffres qui ne sont pas des multiples de 3, 5 ou 15 doivent retourner sa propre valeur, je teste uniquement si le retour est une chaîne de caractères. Pareil que les autres points, le retour de la méthode va être modifié par Pitest et les mutants ne vont pas être tués.
En prenant en compte les commentaires ci-dessus, je vais modifier les tests :
Je génère un nouveau rapport Pitest pour voir le résultat :
Pour conclure, le mutation testing couplé au code coverage sont des bons indicateurs pour faire progresser la qualité du code. Cependant, il n’est pas conseillé de mettre le mutation testing dans la pipeline d’intégration continue, car comme vu précédemment le mutation testing va générer un certain nombre de mutants et devoir exécuter tous les tests unitaires. Cette opération est très coûteuse en temps et en ressources matérielles. Il est vivement conseillé d’utiliser cette méthode à de rares occasions ou dans un pipeline hebdomadaire.
Bon à savoir pour limiter le temps d’exécution de Pitest sur les très gros projets, il est possible de lancer les tests sur un module précis, mais également de définir les mutators qu’on souhaite.
À noter que Pitest est codé en Java et est donc utilisé pour les applications en Java, mais il existe des librairies alternatives pour les autres langages notamment Stryker Mutator pour les langages JavaScript, C# et Scala.