Introduction : Pourquoi 100% de couverture de code est une illusion ?
\nAvez-vous déjà été fier d'un rapport PHPUnit affichant « Code Coverage : 98% » ? Et ensuite, vous avez déployé une version, et un bug critique est apparu en production. Cela vous dit quelque chose ? Le problème est que la couverture classique (line/statement coverage) mesure uniquement quelles lignes de code ont été exécutées. Elle ne vérifie pas si vos tests testent réellement la logique.
\nImaginez que vous ayez écrit un test qui vérifie qu'une fonction retourne true. Et que la fonction elle-même retourne toujours true, même si on la casse. Le test réussira, la couverture sera de 100%, mais vous n'aurez pas détecté le bug. C'est exactement là qu'intervient le mutation testing en PHP — une technique qui bouleverse la conception habituelle des tests.
\nDans cet article, nous allons voir ce qu'est le test de mutation, comment il fonctionne avec un exemple de code PHP réel, et comment l'intégrer dans votre projet avec la bibliothèque Infection.
\n\nQu'est-ce que le Mutation Testing ? (Et pourquoi ce n'est pas de la magie)
\nMutation testing (ou test de mutation) est une méthode d'évaluation de la qualité des tests qui consiste à introduire de petites modifications intentionnelles (mutations) dans le code source. Si après la modification le test échoue — le mutant est tué, le test fonctionne. Si le test reste vert — le mutant a survécu, ce qui signifie que votre test ne vérifie rien.
\nUne analogie simple : vous êtes un gardien, et vos tests sont des caméras. Le mutation testing envoie des « intrus » (mutants) dans le bâtiment. Si la caméra (le test) remarque l'intrus — elle fonctionne. Si l'intrus se promène tranquillement — la caméra est inutile.
\nEn PHP, l'outil principal pour cela est Infection — une bibliothèque qui exécute PHPUnit sur chaque mutant et analyse les résultats.
\n\nComment cela se présente-t-il en pratique ?
\nSupposons que vous ayez une classe de calculatrice simple :
\n<?php\nclass Calculator\n{\n public function add(int $a, int $b): int\n {\n return $a + $b;\n }\n}\nEt son test :
\n<?php\nuse PHPUnit\\Framework\\TestCase;\n\nclass CalculatorTest extends TestCase\n{\n public function testAdd()\n {\n $calc = new Calculator();\n $result = $calc->add(2, 3);\n $this->assertEquals(5, $result);\n }\n}\nL'outil Infection créera un mutant en remplaçant + par - :
return $a - $b;\nEnsuite, il exécutera le test. Si le test échoue — le mutant est tué. Si le test réussit (et il réussira, car vous ne vérifiez que 2+3, pas 2-3) — le mutant a survécu. C'est un signal : votre test n'est pas assez bon.
\n\nInstallation d'Infection et premier lancement
\nPour commencer, installons Infection via Composer :
\ncomposer require --dev infection/infection\nEnsuite, créez un fichier de configuration infection.json (ou exécutez vendor/bin/infection pour une configuration interactive).
Exemple de configuration minimale :
\n{\n "source": {\n "directories": [\n "src"\n ]\n },\n "phpUnit": {\n "configDir": "."\n },\n "mutators": {\n "@default": true\n }\n}\nLancez le test de mutation :
\nvendor/bin/infection --min-msi=100\nOù --min-msi=100 signifie que nous voulons que 100% des mutants soient tués (un objectif idéal, mais rarement atteignable). Infection générera un rapport au format HTML (dossier infection.html), où les mutants survivants seront marqués en rouge.
Types de mutations en PHP (avec exemples)
\nInfection prend en charge des dizaines de mutateurs. Voici les plus courants que vous rencontrerez :
\n\n1. Opérateurs mathématiques
\nRemplacement de + par -, * par /, % par *, etc.
// Code source\nreturn $price * $quantity;\n\n// Mutant\nreturn $price / $quantity;