Tests de mutation en Java : casser le code pour l'améliorer

Formateur Python en ligne pour débutants

Apprenez Python facilement sans surcharge théorique. Résolvez des tâches pratiques avec vérification automatique et écrivez du code directement dans le navigateur.

Start Course

Vos tests mentent, et vous ne le savez pas

\n\n

Imaginez la situation : vous avez écrit des tests unitaires, la code coverage (couverture de code) atteint 95%. Le responsable est satisfait, le pipeline CI/CD est vert. Mais vendredi soir, un bug arrive en production. Cela vous dit quelque chose ?

\n\n

Le problème est que la couverture de code classique est une illusion. Elle montre quelles lignes de code ont été exécutées, mais ne répond pas à la question essentielle : les tests vérifient-ils réellement la logique ? C'est ici qu'intervient le mutation testing — une méthode qui "casse" impitoyablement votre code pour vérifier la qualité de vos tests.

\n\n

Dans cet article, nous allons voir ce qu'est le test de mutation en Java, comment il fonctionne, et montrer des exemples concrets avec l'outil PIT (Pitest). Vous apprendrez à transformer vos tests d'une "simple coche dans le rapport" en une véritable protection contre les bugs.

\n\n

Qu'est-ce que le mutation testing et pourquoi est-ce important ?

\n\n

Le mutation testing (ou test de mutation) est une méthode d'évaluation de la qualité des tests qui consiste à introduire de petites modifications volontairement incorrectes dans le code source (appelées mutations). Ensuite, vos tests sont exécutés. Si au moins un test échoue — le mutant est "tué", le test a fonctionné. Si les tests réussissent — le mutant a "survécu", et c'est un signal d'alarme.

\n\n

Une métaphore simple : vous vérifiez l'efficacité de votre alarme contre les voleurs. Vous introduisez un "voleur" (le mutant) et vous regardez si l'alarme (le test) se déclenche. Si le voleur s'en va tranquillement, l'alarme ne vaut rien.

\n\n

Pourquoi la code coverage est-elle un mauvais indicateur ?

\n\n

Imaginez ce code :

\n\n
public boolean isAdult(int age) {\n    if (age >= 18) {\n        return true;\n    }\n    return false;\n}
\n\n

Et le test :

\n\n
@Test\nvoid testIsAdult() {\n    assertTrue(isAdult(18));\n}
\n\n

La couverture de code affichera 100% (les deux branches if-else sont exécutées ?). Non ! Seule la branche true a été exécutée. Mais le test est vert. Imaginez maintenant que quelqu'un modifie accidentellement la condition en age > 21. Le test passera toujours, car 18 > 21 est faux, et assertTrue attend vrai. Le bug partira en production.

\n\n

Le test de mutation détectera immédiatement cette situation en créant un mutant qui change >= en > ou inverse la condition.

\n\n

Comment fonctionne le mutation testing : théorie simplifiée

\n\n

Le processus comprend trois étapes :

\n\n
    \n
  1. Génération de mutants. L'outil (par exemple, PIT) crée de nombreuses versions de votre code avec de petites modifications : change les opérateurs, supprime les appels de méthodes, inverse les conditions.
  2. \n
  3. Exécution des tests. Pour chaque mutant, vos tests unitaires sont exécutés.
  4. \n
  5. Analyse des résultats. Si un test échoue — le mutant est tué. S'il réussit — le mutant est vivant. Le pourcentage de mutants tués s'appelle le mutation score (score de mutation).
  6. \n
\n\n

Un bon indicateur : 80-90% de mutants tués. Tout ce qui est en dessous est une raison de réécrire les tests.

\n\n

Exemple pratique en Java avec PIT

\n\n

Prenons un exemple concret. Supposons que nous ayons un service de calcul de réduction :

\n\n
public class DiscountService {\n    public double calculateDiscount(double amount, boolean isPremium) {\n        double discount = 0.0;\n\n        if (amount > 100) {\n            discount = 0.1; // 10% de réduction\n        }\n\n        if (isPremium) {\n            discount += 0.05; // 5% supplémentaire\n        }\n\n        // La réduction ne peut pas dépasser 30%\n        if (discount > 0.3) {\n            discount = 0.3;\n        }\n\n        return discount;\n    }\n}
\n\n

Écrivons un test naïf :

\n\n
import org.junit.jupiter.api.Test;\nimport static org.junit.jupiter.api.Assertions.*;\n\nclass DiscountServiceTest {\n\n    private final DiscountService service = new                

Blogs

Book Recommendations