Quand les tests passent, mais que le code est quand même « cassé »
\n\nImaginez la situation : vous avez écrit des tests unitaires pour un projet C#, dotnet test affiche un feu vert, la couverture de code (code coverage) est de 85%. L'équipe est satisfaite, le CI/CD s'exécute parfaitement. Mais un mois plus tard, un bug apparaît en production que les tests unitaires auraient dû, semble-t-il, détecter. Cela vous dit quelque chose ?
Le problème est que la couverture de code (code coverage) est une métrique trompeuse. Elle montre quelles lignes de code ont été touchées, mais ne vérifie pas la qualité des vérifications elles-mêmes. Vous pouvez avoir 100% de couverture, mais aucun assert significatif. C'est ici qu'interviennent les tests de mutation en C# — une méthodologie qui « casse » votre code pour vérifier à quel point vos tests sont capables de détecter les erreurs.
\n\nDans cet article, nous allons voir ce qu'est le mutation testing, comment l'intégrer dans des projets .NET avec Stryker.NET, et pourquoi c'est un must-have pour tout développeur C# sérieux.
\n\nQu'est-ce que le test de mutation (Mutation Testing) ?
\n\nLe test de mutation est une méthode d'évaluation de la qualité des tests, basée sur l'introduction de petites modifications intentionnelles (mutations) dans le code source du programme. Chaque version modifiée du code est appelée un mutant.
\n\nLa logique est simple :
\n- \n
- On prend votre code de production. \n
- Des mutants sont générés automatiquement — par exemple, l'opérateur
+est remplacé par-, la conditionif (x > 5)devientif (x >= 5), ettruedevientfalse. \n - Vos tests unitaires sont exécutés contre chaque mutant. \n
- Si le test échoue — le mutant est « tué » (killed). Bon test. \n
- Si le test réussit — le mutant a « survécu » (survived). Mauvais signe : votre test n'a pas remarqué le changement de logique. \n
L'objectif est d'atteindre un pourcentage élevé de mutants tués (généralement 80%+). C'est une garantie réelle que vos tests vérifient le comportement, et non pas simplement « parcourent » le code.
\n\nPourquoi la couverture de code (code coverage) est-elle insuffisante ?
\n\nPrenons un exemple simple en C#. Supposons que nous ayons une méthode qui vérifie si un nombre est pair :
\n\npublic class MathUtils\n{\n public bool IsEven(int number)\n {\n return number % 2 == 0;\n }\n}\n\nEt un test « idéal » avec 100% de couverture :
\n\n[Test]\npublic void IsEven_ShouldReturnTrueForEvenNumber()\n{\n var utils = new MathUtils();\n var result = utils.IsEven(4);\n Assert.IsTrue(result);\n}\n\nLe test est vert, la couverture est de 100%. Mais que se passe-t-il si quelqu'un modifie accidentellement (ou intentionnellement) la méthode en return number % 2 == 1; ? Notre unique test échouera-t-il ? Non ! Il testera le nombre 4 (4 % 2 == 0, mais maintenant la méthode retournera false), l'assert échouera — le mutant sera tué. Tout va bien ? Attendez.
Et si on modifie le code en return number % 2 != 1; ? Logiquement, c'est la même chose, mais le test de mutation créera un mutant : return number % 2 != 0;. Notre test avec le nombre 4 réussira toujours (4 % 2 != 0 — false, la méthode retournera false, mais l'assert attend true — le test échouera). Tout va bien ici. Mais imaginez que nous ayons écrit le test ainsi :
[Test]\npublic void IsEven_ShouldNotThrow()\n{\n var utils = new MathUtils();\n Assert.DoesNotThrow(() => utils.IsEven(4));\n}\n\nLa couverture est toujours de 100%, mais l'assert ne vérifie pas le résultat ! Maintenant, n'importe quel mutant survivra. Le test de mutation montrera immédiatement que vos tests sont des coquilles vides.
\n\nOutils pour le test de mutation en C#
\n\nL'outil principal et pratiquement standard pour .NET est Stryker.NET. C'est un framework open-source qui s'intègre facilement dans n'importe quel projet C#. Il supporte xUnit, NUnit et MSTest.
\n\nAutres outils (moins populaires) :
\n- \n
- VisualMutator — un plugin obsolète pour Visual Studio. \n
- Nester \n