Dans cet article, on va voir avec un cas concret ce qu’est le mutation testing et comment ça peut vous être utile au quotidien 😀. Sans plus tarder, prenons l’exemple suivant:
On demande à Bob de créer un programme pour déterminer si un animal est mignon avec les règles suivantes:
Par défaut un animal n’est pas mignon ❌
Si un animal a 2 yeux et 4 pattes alors il est mignon ✅
Bob fait des implémentations douteuses mais il est consciencieux et fait des tests unitaires après son dev’ 👏
public class CuteServiceTest {
private final CuteService cuteService = new CuteService();
@Test
public void shouldNotBeCuteWithNotEnoughLegs(){
Animal animal = new Animal();
animal.setAnimalType(AnimalType.SNAKE);
animal.setNumberOfLegs(0);
animal.setNumberOfEyes(2);
assertThat(cuteService.isCute(animal)).isFalse();
}
@Test
public void shouldBeCuteWithRightNumberOfLegsAndEyes(){
Animal animal = new Animal();
animal.setAnimalType(AnimalType.CAT);
animal.setNumberOfLegs(4);
animal.setNumberOfEyes(2);
assertThat(cuteService.isCute(animal)).isTrue();
}
}
Bob, run ses tests avec le coverage linéaire et il obtient le résultat suivant:
Bob est satisfait de sa couverture de test et décide que c’est l’exemple parfait pour montrer au stagiaire que les tests unitaires c’est important.
Il retourne dans le code du service, change la valeur de la condition:
et relance ses tests mais catastrophe, les tests sont toujours verts !
Explication 🕵️
Le test shouldBeCuteWithRightNumberOfLegsAndEyes sera vrai peu importe la première condition comme c’est un chat. Le test nous donne un faux sentiment de confiance: On a l’impression de tester la logique de la méthode alors qu’on ne teste qu’une portion de celle-ci.
Revenons au mutation testing
Le mutation testing va automatiser ce que vient de faire Bob manuellement: L’idée est de changer le code et de vérifier que les tests échouent bien quand ils le doivent.
Le mutation testing consiste à modifier votre code (changement de condition, de bornes dans les boucles, de valeurs etc..). On appelle ces modifications des mutants 👾.
Ensuite vos tests unitaires sont joués sur lecode muté.
Si le test est vert, la mutation a survécue. (c’est généralement pas ce qu’on souhaite)
Si le test est rouge, la mutation a été tuée.
Un rapport est ensuite généré et va nous indiquer la pertinence de nos tests unitaires. Plus un test tue de mutation, plus il est robuste.
Voici un exemple de rapport généré par l’outil de mutation testing que j’utilise (PIT Mutation testing)
Ici, on voit dans le rapport que j’ai un test qui n’a pas échoué alors que la condition ligne 8 a été inversée, ce qui peut indiquer que cette portion n’est pas bien testée par nos tests unitaires.
En conclusion
Le mutation testing est un outil très pratique qui va vous permettre de mesurer la robustesse de vos tests unitaires en comparant leur résultat lorsqu’ils s’exécutent sur du code automatiquement modifié.
Le seul inconvénient que je vois au mutation testing est le fait que ça soit un processus qui peut prendre pas mal de temps si on veut l’exécuter sur tout un projet (mais on peut filtrer sur un package ou sur une classe en particulier au besoin).
Sur certains projets, j’ai constaté que des parties complexes étaient mal testées car moins de 20% des mutations générées sur ces parties étaient détectées par les tests unitaires. Il s’est avéré que ces parties n’étaient pas correctement testées donc je trouve cet outil particulièrement intéressant au quotidien !
Notes complémentaires
– Le problème qu’on a simulé en exemple n’aurait pas pu être introduit avec du TDD. La phase “rouge” existe précisément pour éviter de créer un test “vert pour les mauvaises raisons”. Si vous ne connaissez pas le TDD je vous conseille cet excellent article: https://ippon.developpez.com/tutoriels/alm/apprendre-astuces-tdd/
– Le code coverage linéaire est une mesure très limitée, le coverage via tracing/branching est bien plus pertinent car il va indiquer si vous avez couvert les cas vrais et faux d’une condition. Pour voir un exemple: https://www.youtube.com/watch?v=zyM2Ep28ED8&t=328s
Il arrive que la code review nous empêche de merge nos PR assez vite et qu’on se retrouve à tirer une branche d’une branche de travail pour avancer 😔
Une fois la première PR squash et merge, la PR issue de la seconde branche se retrouve avec des conflits 💥
Si vous avez déjà vécu cette situation, il y a de bonnes chances que vous ayez cherry-pick vos commits de travail sur une...
Il arrive souvent que pour tester unitairement des règles de validation, on doive tester le même cas mais avec des exemples différents. Sans tests paramétrés, ça revient à faire un test par cas ce qui peut alourdir notre fichier de tests.
On va voir comme...
Avant .Net 8, tester du code qui utilise DateTime.Now() n’était pas trivial, on devait faire en sorte de mocker la Clock dans nos tests. Depuis .Net 8, c’est beaucoup plus facile grâce à TimeProvider inclut par défaut !
En deux mots, TimeProvider est une...
La gestion des erreurs de son API est très importante pour que les consommateurs puissent avoir une description claire du problème mais c’est souvent fastidieux à maintenir.
On va voir comment avoir des statuts de réponse cohérent et des messages d’erre...