Tests de mutation en C# : casser le code pour l'améliorer

Formateur Python en ligne pour débutants

Apprenez Python facilement sans surcharge théorique. Résolvez des tâches pratiques avec vérification automatique et écrivez du code directement dans le navigateur.

Start Course

Quand les tests passent, mais que le code est quand même « cassé »

\n\n

Imaginez 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 ?

\n\n

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\n

Dans 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\n

Qu'est-ce que le test de mutation (Mutation Testing) ?

\n\n

Le 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\n

La 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 condition if (x > 5) devient if (x >= 5), et true devient false.
  • \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
\n\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\n

Pourquoi la couverture de code (code coverage) est-elle insuffisante ?

\n\n

Prenons un exemple simple en C#. Supposons que nous ayons une méthode qui vérifie si un nombre est pair :

\n\n
public class MathUtils\n{\n    public bool IsEven(int number)\n    {\n        return number % 2 == 0;\n    }\n}
\n\n

Et 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\n

Le 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.

\n\n

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 :

\n\n
[Test]\npublic void IsEven_ShouldNotThrow()\n{\n    var utils = new MathUtils();\n    Assert.DoesNotThrow(() => utils.IsEven(4));\n}
\n\n

La 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\n

Outils pour le test de mutation en C#

\n\n

L'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\n

Autres outils (moins populaires) :

\n
    \n
  • VisualMutator — un plugin obsolète pour Visual Studio.
  • \n
  • Nester
  • \n

Blogs

Book Recommendations