Êtes-vous sûr que vos tests fonctionnent vraiment ?
\n\nImaginez : vous avez 95% de couverture de code, tous les tests sont verts, le CI/CD passe sans erreur. L'équipe est satisfaite, le manager est serein. Mais un jour, un bug apparaît en production. Vous ouvrez le code, regardez les tests et comprenez : ils vérifient simplement que la fonction retourne un nombre, sans regarder sa valeur. Ou pire — le test est écrit de telle manière qu'il réussit dans tous les cas.
\n\nUne situation familière ? Bienvenue dans le monde où le pourcentage de couverture n'est qu'un chiffre, pas une garantie de qualité. C'est là qu'intervient le test de mutation (mutation testing) — une méthode qui fait vraiment souffrir vos tests et les oblige à prouver leur valeur.
\n\nDans cet article, nous verrons comment intégrer le mutation testing dans un projet TypeScript en utilisant Stryker, et pourquoi cela pourrait devenir votre nouvel outil préféré (ou une nouvelle source de maux de tête).
\n\nQu'est-ce que le test de mutation en termes simples ?
\n\nLe test de mutation, c'est quand vous prenez votre code, y introduisez délibérément de petites erreurs (mutations) et regardez si vos tests échouent. Si les tests ne tombent pas — vous testez mal. S'ils tombent — bravo, le mutant est éliminé.
\n\nÇa semble sadique ? Peut-être. Mais ça marche. Imaginez que vous êtes un ingénieur QA qui s'est furtivement glissé dans le code et a remplacé === par ==, > par < ou a supprimé un appel de fonction. Un bon test doit le détecter. Un mauvais test le laissera passer.
En TypeScript, avec son typage strict, le test de mutation est particulièrement utile : il détecte les cas où les types sont corrects, mais la logique ne l'est pas.
\n\nPourquoi la couverture de code classique (code coverage) est une illusion ?
\n\nSupposons que vous ayez une fonction :
\n\nfunction calculateDiscount(price: number, isPremium: boolean): number {\n if (isPremium) {\n return price * 0.8;\n }\n return price;\n}\n\nEt un test :
\n\ntest('returns number', () => {\n const result = calculateDiscount(100, true);\n expect(typeof result).toBe('number');\n});\n\nCouverture ? 100%. Test ? Réussi. Et si on remplace price * 0.8 par price * 0.5 ? Le test réussira quand même — il ne vérifie que le type, pas la valeur. Voilà un faux sentiment de sécurité.
Le test de mutation mettra en évidence une telle mutation et dira : « Hé, ton test n'a pas remarqué que j'ai changé la réduction ! ». Et vous devrez ajouter une vérification correcte.
\n\nComment fonctionne le mutation testing en TypeScript : étape par étape
\n\nL'ensemble du processus peut être divisé en trois étapes :
\n\n- \n
- Génération de mutants — l'outil copie votre code et apporte une modification à la fois. Par exemple, il change
&&en||ou supprime un appel de méthode. \n - Exécution des tests — pour chaque mutant, vos tests sont lancés. \n
- Analyse des résultats — si les tests échouent, le mutant est « tué ». Sinon, il a « survécu », et c'est une zone de risque. \n
La métrique principale est le mutation score (pourcentage de mutants tués). 100% est l'idéal, mais en pratique, 80-90% est déjà excellent.
\n\nL'outil n°1 pour TypeScript : Stryker
\n\nDans le monde de TypeScript et JavaScript, le roi du test de mutation est Stryker (StrykerJS). Il est rapide, fonctionne avec Jest, Vitest, Mocha et même avec les tests navigateur. Configuration en 5 minutes.
\n\nInstallation :
\n\nnpm install --save-dev @stryker-mutator/core @stryker-mutator/jest-runner\n\nCréons le fichier de configuration stryker.conf.js :
// stryker.conf.js\nexport default {\n mutate: ['src/**/*.ts', '!src/**/*.spec.ts'], // quoi muter\n testRunner: 'jest',\n reporters: ['html', 'clear-text', 'progress'],\n jest: {\n projectType: 'custom',\n configFile: 'jest.config.ts',\n },\n concurrency: 4, // combien de mutants