Introduction : les tests passent, mais les bugs persistent ?
\n\nImaginez la situation : vous avez écrit un module en Go, couvert par des tests unitaires à 95% selon go test -cover, tous les tests sont verts. Vous êtes convaincu que le code fonctionne parfaitement. Mais en production, un bug apparaît. Cela vous dit quelque chose ?
Le problème est que la métrique de couverture de code (code coverage) n'est qu'un nombre de lignes qui ont été exécutées. Elle ne dit pas ce que les tests vérifient réellement. On peut avoir 100% de couverture sans jamais vérifier qu'une fonction retourne la bonne valeur, et non pas simplement qu'elle ne plante pas avec une panique.
\n\nC'est là qu'intervient le test de mutation (mutation testing) en Go — une méthode qui introduit artificiellement des erreurs dans votre code et vérifie si les tests peuvent les détecter. Si les tests ne tombent pas sur le code muté, cela signifie que vos tests sont inutiles.
\n\nDans cet article, nous allons voir ce qu'est le test de mutation en Go, comment le configurer, et pourquoi c'est un must-have pour les projets où la fiabilité est critique. C'est parti !
\n\nQu'est-ce que le test de mutation ?
\n\nLe test de mutation est une méthode d'évaluation de la qualité des tests qui consiste à introduire de petites modifications (mutations) dans le programme, simulant des erreurs typiques de développeur. Ensuite, les tests sont exécutés. Si au moins un test échoue, la mutation est "tuée", le test fonctionne. Si tous les tests réussissent, la mutation a "survécu", et c'est un signal d'alarme : vous avez une faille dans vos tests.
\n\nEn Go, le test de mutation est souvent appelé mutation testing golang, et il est particulièrement utile pour vérifier la logique métier, les validateurs, les analyseurs et autres composants critiques.
\n\nExemple de mutation
\n\nSupposons que vous ayez une fonction simple :
\n\nfunc IsEven(n int) bool {\n return n%2 == 0\n}\n\nUne mutation typique consiste à remplacer == par != :
func IsEven(n int) bool {\n return n%2 != 0\n}\n\nSi votre test vérifie seulement que IsEven(2) ne plante pas, mais ne vérifie pas que le résultat est true, alors la mutation survivra. Le test passera, même si la fonction fonctionne désormais à l'envers.
Pourquoi la simple couverture de code ne suffit-elle pas ?
\n\nDe nombreux développeurs Go ont l'habitude de se fier à go test -cover. C'est une métrique utile, mais elle est trompeuse. Prenons un exemple :
func Divide(a, b int) (int, error) {\n if b == 0 {\n return 0, errors.New("division by zero")\n }\n return a / b, nil\n}\n\nTest :
\n\nfunc TestDivide(t *testing.T) {\n result, err := Divide(10, 2)\n if err != nil {\n t.Fatal(err)\n }\n if result != 5 {\n t.Errorf("expected 5, got %d", result)\n }\n}\n\nCouverture ? 100% — toutes les lignes ont été exécutées. Mais que se passe-t-il si quelqu'un remplace accidentellement a / b par a * b ? Le test ne le remarquera pas, car 10 * 2 = 20, alors que nous attendions 5. Le test échouera-t-il ? Non, si nous ne vérifions pas la valeur exacte.
Le test de mutation en Go identifiera immédiatement ce problème en créant un mutant avec la multiplication et en vérifiant si le test échoue.
\n\nOutils pour le test de mutation en Go
\n\nL'outil principal est go-mutesting. C'est une bibliothèque populaire qui crée automatiquement des mutations et exécute les tests. Installation :
\n\ngo get github.com/zimmski/go-mutesting/cmd/go-mutesting\n\nIl existe aussi des alternatives, comme mutesting de la société "Go Test It", mais go-mutesting est la référence de facto.
Comment l'exécuter
\n\nExécution de base :
\n\ngo-mutesting ./...\n\nCette commande trouvera tous les fichiers _test.go dans le projet et vérifiera quelles mutations survivent. Le résultat s'affiche dans la console avec un rapport détaillé.
Exemple pratique : test de mutation en Go
\n\nCréons un package simple calculator avec des fonctions d'addition et