Test par mutation avec Pit Test
Je prends la plume avec émotion pour la première fois pour vous parler de tests unitaires et de qualité de tests.
En termes de qualité de tests unitaires, l’indicateur principal auquel on pense est souvent le taux de couverture. Dans l’absolu on se dit que quand c’est tout vert, c’est tout bon, tant que c’est un peu rouge, c’est moins bon.
Je vous propose de découvrir ensemble les tests par mutation, qui sont un outil permettant de vérifier la pertinence des tests unitaires et de la couverture de code. Nous verrons aussi une implémentation dans notre langage préféré (Java, évidemment) : Pit test.
Tests par mutation, what is it ?
Tout d’abord, il faut savoir que les conditions sine qua non d’utilisation des tests par mutation sont :
- D’avoir des tests unitaires (c’est une évidence telle que j’en ai presque honte de l’écrire !!)
- D’avoir une « bonne » couverture de test
Le principe du test par mutation est le suivant : des modifications sont apportées sur le code à tester : on appelle ça un mutant (rien à voir avec ça …). À partir de ce mutant, les tests unitaires sont lancés : si au moins un des tests échoue, on considère que le mutant a été tué. Si au contraire il survit, les tests unitaires ne sont pas suffisamment complets.
On peut ainsi mesurer un pourcentage de mutants tués et de survivants afin d’obtenir un indicateur sur la qualité des tests.
De la même façon que le taux de couverture de tests ne doit pas être juste un objectif à atteindre, le taux de mutation doit être perçu comme un axe d’amélioration de la qualité de code (des chefs de projets mal intentionnés pourraient vouloir dévoyer ces taux pour mettre des primes sur objectifs aux dev si le taux est atteint : ça n’est surtout pas le but…)
Exemple :
Prenons un exemple volontairement simpliste pour bien comprendre avec une méthode très complexe à tester :
public int getFoo(int bar) { if (bar == 3) { return 5; } else { return 2; } }
Admettons que mon test (lui aussi très simpliste) soit le suivant :
@Test public void shouldBePositive() { assertTrue(getFoo(0) > 0); assertTrue(getFoo(3) > 0); }
Mon test passe avec succès, tout va bien, mon code est beau, ma couverture est verte, les oiseaux chantent, c’est super… Sauf que… Sauf que votre œil avisé aura probablement remarqué que mon test est pourri biaisé et qu’il ne teste rien ou presque.
C’est là qu’interviennent les mutants !
Imaginons un mutant (appelons le ‘Negate Conditionals’, c’est moins marrant que Dédé mais il a pas choisi) qui pourrait ressembler à ça :
public int getFoo(int bar) { if (bar != 3){ return 5; } else{ return 2; } }
(Notez la condition qui a changé dans le code, on a changé le == par un !=).
Si je relance ma test suite sur ce code muté, elle passe avec succès, ma couverture est encore verte, le mutant a survécu !
Le cas présent ici est très simple, le but étant juste de comprendre le concept.
Un outil pour Java : Pit Test
Pit test permet d’appliquer toute une série de mutants sur le code, de lancer les suites de tests et de sortir un rapport plutôt bien fourni.
L’utilisation est d’une simplicité absolue : il faut simplement ajouter un plugin maven ou gradle, lancer une commande et hop, on a notre rapport !
Gradle:
dans le build.xml, ajouter la dépendance suivante :
buildscript { dependencies { classpath 'info.solidsoft.gradle.pitest:gradle-pitest-plugin:1.1.6' } }
Appliquer le plugin :
apply plugin: "info.solidsoft.pitest"
Et ensuite, lancer pit:
gradle pitest
Maven :
C’est l’exemple sur le site de pitTest, je ne l’ai pas essayé en vrai, mais ça devrait marcher pareil :
<plugin> <groupId>org.pitest</groupId> <artifactId>pitest-maven</artifactId> <version>LATEST</version> </plugin>
Et pour lancer pit :
mvn org.pitest:pitest-maven:mutationCoverage
Résultats :
Une fois le rapport HTML généré, il faut ensuite savoir quoi en faire, voici à quoi il ressemble :
On voit en rouge les mutants qui ont survécu, en vert ceux qui ont été tués. On note de façon assez claire que notre test peut être amélioré.
Rajoutons un test unitaire à notre classe de test :
@Test public void shouldBeTheRightValue() { assertEquals(5, bean.getFoo(3)); }
Je relance pit : c’est mieux, le negated conditional a été tué (à mort!).
Il reste encore du rouge, je vous laisse le soin d’améliorer le test. À ce stade là, vous devriez avoir compris le principe !
Différents types de mutants :
Je vous fais une très brève description des différents types de mutants que vous rencontrerez dans PIT :
Les mutants « par défaut » :
- Conditionals Boundary Mutator : Change une condition en ajoutant ou supprimant le =. Par exemple < devient <=
- Increments Mutator : Modifie les incrémentations par des décrémentations (i++ devient i–).
- Invert Negatives Mutator : Inverse les numériques (i devient -i)
- Math Mutator : Change les opérations mathématiques (par exemple + devient -).
- Negate Conditionals Mutator : Inverse les conditions (== devient !=).
- Return Values Mutator : modifie les valeurs de retour des méthodes (par ex. return new Object() devient return null).
- Void Method Calls Mutator : Supprime les appels à des méthodes dont le type de retour est void.
Les mutants « supplémentaires » :
- Constructor Calls Mutator : Remplace les appels à un constructeur par null.
- Inline Constant Mutator : Change des valeurs de constantes.
- Non Void Method Calls Mutator : Supprime des appels à des méthodes dont le type de retour n’est pas void.
- Remove Conditionals Mutator : Modifie des conditions en mettant true à la place
Il y a aussi des mutants qualifiés d’« expérimentaux », je ne me lance pas ici dans leur présentation (un « mutant expérimental », le terme fout quand même un peu la trouille)
Quelques options utiles au plugin gradle :
- enableDefaultIncrementalAnalysis (true, false) : Permet d’éviter de relancer la totalité des mutants si les tests de sont pas modifiés.
- testSourceSets = [sourceSets.test, sourceSets.integrationTest] : Définie les sources de tests
- excludedMethods = [« hashCode »] : Exclue certaines méthodes de l’analyse
- mutators = [« DEFAULTS », « REMOVE_CONDITIONALS », « NON_VOID_METHOD_CALLS »] : sélection des mutants
- timestampedReports = false : Par défaut, pit génère chaque rapport dans un répertoire timestampé. Cette option permet de générer le rapport toujours au même endroit.
Quelques liens utiles :
- La définition très exhaustive des tests par mutation du wiki : https://en.wikipedia.org/wiki/Mutation_testing
- Le site de pit test : http://pitest.org/
- La liste des mutants disponibles sur pit test : http://pitest.org/quickstart/mutators/
- Quelques informations sur le plugin gradle qui va bien : http://gradle-pitest-plugin.solidsoft.info
Conclusion
Les tests par mutation sont, comme on l’a vu, un outil supplémentaire dans la qualité du code qui s’ancre complètement dans la démarche d’amélioration continue qui est la nôtre !
Je vous invite vivement à jouer avec Pit test mutator, la mise en place est vraiment simple : essayez le sur vos projets, le premier résultat est parfois assez étonnant…
A bientôt pour de nouvelles aventures !
Sam