Vos tests mentent-ils ? Passez au mutation testing
\n\nVous écrivez des tests unitaires, la couverture de code (code coverage) est belle — 85 %, 90 %, mais les bugs refont surface en production ? Un scénario classique. La métrique de couverture traditionnelle (line/branch coverage) ne fait que mesurer quelles lignes de code ont été exécutées. Mais elle ne dit rien sur la qualité réelle de vos tests. Ceux-ci peuvent simplement "passer" sur les lignes sans vérifier la logique métier.
\n\nC'est là qu'intervient le mutation testing (test de mutation). Pensez-y comme à un vaccin pour votre code : on introduit intentionnellement de petits "virus" (mutations) dans le programme, et on observe si nos tests les détectent. Si un test ne plante pas après avoir changé === en !==, c'est que vos tests sont trop faibles. Dans cet article sur pythonlib.fr, nous allons voir comment intégrer le mutation testing dans un projet JavaScript pour rendre votre code vraiment fiable.
Qu'est-ce que le mutation testing en termes simples
\n\nImaginez que vous êtes un ingénieur QA qui modifie en secret une ligne de code du développeur :
\n\n// Avant\nif (user.age >= 18) { allowAccess(); }\n\n// Après (mutation)\nif (user.age > 18) { allowAccess(); }\n\nSi après ce changement, les tests unitaires sont toujours verts, c'est qu'ils ne vérifient pas la condition limite 18. Un framework de mutation effectue des milliers de modifications de ce type automatiquement et génère un rapport : combien de mutations ont survécu (survived). Moins il y a de mutants survivants, plus vos tests sont robustes.
Les principaux types de mutations en JavaScript :
\n- \n
- Modification des opérateurs de comparaison :
>→>=,===→!==\n - Suppression d'appels de fonctions :
validate()→ chaîne vide \n - Modification des littéraux numériques :
0→1,100→0\n - Inversion de conditions booléennes :
if (isActive)→if (!isActive)\n - Remplacement de valeurs de chaînes (plus rare, mais possible) \n
Pourquoi la couverture de code est une fausse sécurité
\n\nDe nombreux développeurs JavaScript se laissent berner par une couverture de code à 100 %. Regardons un exemple concret :
\n\n// Fonction qui calcule une réduction\nfunction getDiscount(price, isPremium) {\n if (price > 100 && isPremium) {\n return price * 0.8;\n }\n return price;\n}\n\nEt le test "parfait" :
\n\ntest('devrait retourner le prix pour un non-premium', () => {\n expect(getDiscount(50, false)).toBe(50);\n});\n\ntest('devrait retourner le prix pour un premium avec petit prix', () => {\n expect(getDiscount(50, true)).toBe(50);\n});\n\nCouverture : 100 % des lignes, 100 % des branches. Mais la mutation price > 100 → price >= 100 survivra ! Les tests ne vérifient pas la valeur limite 100.01 ou exactement 100. Le mutation testing mettra immédiatement en évidence cette "zone d'ombre".
Outil n°1 : Stryker Mutator pour JavaScript
\n\nL'outil le plus populaire et mature pour le mutation testing dans le monde JS est Stryker Mutator (ou simplement Stryker). Il prend en charge Jest, Mocha, Jasmine, Karma et fonctionne avec TypeScript.
\n\nInstallation et démarrage rapide
\n\nInstallez Stryker globalement ou localement dans votre projet :
\n\nnpm install --save-dev @stryker-mutator/core @stryker-mutator/jest-runner\n\nCréez un fichier de configuration stryker.conf.json :
{\n "$schema": "./node_modules/@stryker-mutator/core/schema/stryker-schema.json",\n "packageManager": "npm",\n "reporters": ["html", "clear-text", "progress"],\n "testRunner": "jest",\n "coverageAnalysis": "perTest",\n "thresholds": {\n "high": 80,\n "low": 60,\n "break": 50\n }\n}\n\nLancez le test de mutation