Si votre suite de mutation testing met quatre heures à s’exécuter, félicitations. Vous avez prouvé ce que tout le monde soupçonnait déjà : votre suite de tests a des lacunes.
Vous ne l’exécuterez pas en CI à chaque push. Aucune équipe ne le fait. La question n’est pas de savoir si vous pouvez vous permettre quatre heures par commit. C’est de savoir si vous pouvez vous permettre d’expédier du code avec des tests qui passent mais ne vérifient en réalité rien du tout.
Une couverture de code à 100 % est une métrique de vanité
La couverture de code mesure quelles lignes ont été exécutées pendant les tests. Elle ne mesure pas si ces lignes ont été testées correctement.
Un test peut exécuter une ligne, ne rien vérifier de significatif, et compter quand même comme couvert. Le mutation testing corrige cela en apportant de petites modifications à votre code, en exécutant les tests, et en vérifiant s’ils échouent. Si un test passe après que le code a été délibérément cassé, ce test ne vaut rien.
Le problème, c’est l’échelle. Un projet JavaScript de taille moyenne avec 10 000 lignes de code et 500 tests peut générer 8 000 mutations. Exécuter la suite de tests complète contre chaque mutation est coûteux en calcul. Sur un runner CI typique, c’est là que viennent vos quatre heures.
Exécuter la suite complète à chaque commit est impossible. Mais cela ne signifie pas que vous devez abandonner complètement le mutation testing.
Le mutation testing incrémental est la seule approche pratique
Les outils modernes de mutation testing supportent l’analyse incrémentale. Au lieu de muter l’ensemble de la base de code, ils ne mutent que le code qui a changé dans la pull request actuelle.
Pour une PR typique avec 200 lignes de code modifiées, l’outil peut générer de 40 à 80 mutations. Exécuter le sous-ensemble de tests pertinent contre ces mutations prend des minutes, pas des heures. C’est ainsi que les équipes utilisent réellement le mutation testing en CI.
StrykerJS, l’un des frameworks de mutation testing JavaScript les plus utilisés, supporte le mode incrémental via son option incremental. Il stocke les résultats de mutation dans un fichier incremental.json et ne réanalyse que les fichiers modifiés.
Voici un stryker.conf.json minimal configuré pour des exécutions incrémentales en CI :
{
"packageManager": "npm",
"reporters": ["html", "clear-text", "json"],
"testRunner": "jest",
"coverageAnalysis": "perTest",
"incremental": true,
"incrementalFile": "reports/stryker-incremental.json",
"mutate": [
"src/**/*.js",
"!src/**/*.test.js",
"!src/**/__tests__/**"
],
"thresholds": {
"high": 80,
"low": 60,
"break": 50
}
}
Le paramètre coverageAnalysis: perTest est critique. Il indique à Stryker de n’exécuter que les tests qui couvrent chaque fichier muté, et non la suite entière. Cela seul peut réduire le temps d’exécution d’un ordre de grandeur.
Le bloc thresholds définit quand le build échoue. Dans cet exemple, un mutation score inférieur à 50 % fait échouer le pipeline CI. Les scores entre 50 % et 60 % produisent un avertissement. Au-dessus de 80 %, c’est vert.
Trois patterns CI qui fonctionnent vraiment
Les équipes qui utilisent le mutation testing avec succès ne tentent pas de l’exécuter comme des tests unitaires. Elles utilisent l’un des trois patterns suivants.
Exécutions complètes nocturnes sur la branche principale. La suite de mutation complète s’exécute une fois par jour, généralement la nuit. Les résultats sont publiés sur un dashboard et suivis dans le temps. Cela permet de détecter les problèmes systémiques de qualité des tests sans bloquer le développement quotidien. L’équipe examine les tendances, pas les scores individuels.
Exécutions incrémentales sur les pull requests. Seuls les fichiers modifiés sont mutés. Le job CI ajoute de 3 à 8 minutes au pipeline de la PR. Si le mutation score du code modifié tombe sous le seuil, la PR est bloquée. C’est là que le mutation testing prend tout son sens : au moment où le nouveau code entre dans la base de code.
Gardes pré-release avant les déploiements majeurs. Certaines équipes exécutent une analyse de mutation complète avant de livrer en production ou avant de publier une nouvelle version. C’est traité comme un point de contrôle qualité, similaire à un audit de sécurité ou un test de régression de performance. Pas à chaque release, mais pour celles qui comptent.
Les équipes qui en tirent le plus de valeur mélangent les deux premiers patterns. Les exécutions nocturnes suivent la santé de l’ensemble de la base de code. Les exécutions incrémentales sur les PR imposent la qualité sur le nouveau code.
Le mutation score n’est pas une cible
C’est là que le mutation testing devient politiquement dangereux. Si vous publiez un mutation score à l’échelle de l’équipe et que vous le liez aux évaluations de performance, les ingénieurs vont optimiser pour la métrique.
Ils écriront des tests qui tuent des mutations sans tester le comportement réel. Ils argumenteront que les mutants équivalents, sémantiquement identiques au code original, devraient être exclus du score. Ils passeront des heures à ajuster des seuils au lieu d’écrire des tests utiles.
Le mutation testing est un outil de diagnostic, pas un classement. Le score est un signal à investiguer, pas une cible à atteindre.
Une approche plus utile consiste à suivre la tendance du mutation score dans le temps et à considérer les scores bas sur le nouveau code comme un point de départ de conversation. « Cette PR introduit 12 mutations et seulement 4 sont tuées. Regardons ce qui manque. » C’est infiniment plus précieux qu’un dashboard affichant 73 % sur l’ensemble du repository.
Un workflow GitHub Actions fonctionnel
Voici ci-dessous un workflow GitHub Actions prêt pour la production qui exécute du mutation testing incrémental sur les pull requests et stocke l’état incrémental entre les exécutions.
name: Mutation Testing
on:
pull_request:
branches: [main]
jobs:
stryker:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: actions/setup-node@v4
with:
node-version: 22
cache: "npm"
- name: Install dependencies
run: npm ci
- name: Download previous incremental report
uses: actions/download-artifact@v4
with:
name: stryker-incremental
path: reports/
continue-on-error: true
- name: Run Stryker (incremental)
run: npx stryker run
- name: Upload incremental report for next run
uses: actions/upload-artifact@v4
with:
name: stryker-incremental
path: reports/stryker-incremental.json
if: always()
Le détail clé est fetch-depth: 0. Stryker a besoin de l’historique Git complet pour déterminer quels fichiers ont changé entre la branche de la PR et la branche cible. Sans cela, le mode incrémental revient à une exécution complète.
Le workflow télécharge l’artefact stryker-incremental.json précédent avant de s’exécuter. Si l’artefact n’existe pas, la première exécution est effectivement une analyse complète. Les exécutions suivantes utilisent les résultats mis en cache.
Le if: always() sur l’étape d’upload garantit que l’état incrémental est sauvegardé même si le job de mutation testing échoue à cause d’un dépassement de seuil. Sans cela, la PR suivante repart de zéro.
Les mutants équivalents restent un problème
Aucun outil de mutation testing ne peut détecter de manière fiable les mutants équivalents. Ce sont des mutations qui changent la syntaxe du code mais pas sa sémantique. Un exemple classique est de remplacer a = b + c par a = c + b dans une opération commutative. La mutation est techniquement différente, mais le comportement est identique.
Les mutants équivalents gaspillent du temps CI et frustrent les ingénieurs. L’état de l’art actuel est l’exclusion manuelle via une configuration spécifique à l’outil. Stryker vous permet d’ignorer des mutateurs ou des fichiers spécifiques. PIT pour Java supporte excludedMethods et excludedClasses.
Il n’y a pas de solution parfaite. Les équipes qui utilisent le mutation testing acceptent un niveau de bruit de base et révisent périodiquement leurs listes d’exclusion.
Votre équipe devrait-elle s’y mettre ?
Le mutation testing n’est pas gratuit. Il nécessite du compute CI, de la configuration d’outil, et une maintenance continue des seuils et des exclusions. C’est du surmenage pour un prototype ou un projet avec deux ingénieurs.
Cela en vaut la peine quand vous avez une base de code suffisamment grande pour que la qualité des tests se dégrade sans surveillance, et une équipe suffisamment grande pour que tout le monde ne révise pas chaque PR en détail. Si vous avez déjà trouvé un bug en production qui aurait dû être détecté par un test, et que le test existe mais ne vérifie en réalité rien, le mutation testing l’aurait détecté.
Commencez par des exécutions incrémentales sur les PR pour votre service le plus critique. Suivez la tendance pendant un mois. Si les chiffres vous disent quelque chose d’utile, étendez-vous. Sinon, vous avez perdu quelques minutes de CI, pas quatre heures.
Pour les équipes qui débutent, le Stryker handbook propose des guides spécifiques à chaque plateforme pour JavaScript, C# et Scala. Pour les projets JVM, PIT reste la norme. Les deux supportent l’analyse incrémentale nativement.