De bons tests doivent échouer : assurez-vous que des tests significatifs avec Pitest
Le problème avec la couverture de test standard
L’écriture de tests unitaires est une partie essentielle du processus de développement. Nous utilisons souvent des outils de reporting tels que jacoco pour vérifier la couverture du test. Si nous voyons que toutes les lignes sont vertes, nous sommes heureux d’avoir tout couvert.
Cependant, une couverture complète peut être obtenue sans aucun test significatif. Par exemple, je peux écrire des tests sans aucune assertion ni vérification. Parfois, même si nous couvrons tous les cas, ils peuvent ne pas être assez exhaustifs.
Comment améliorer la qualité des tests ?
Test de mutation est une technique qui vérifie si chaque morceau de code est testé de manière significative. Il modifie le code en mémoire de diverses manières pour produire des résultats différents. Ensuite, il s’avère si les tests échoueront. Les bons tests devraient échouer. Nous considérons que le test est réussi si les mutants (modifications de code) sont tués.
L’image suivante illustre comment la couverture des lignées et des mutations peut différer :
Dans cet article, je vais vous présenter le Pitesti (PIT) cadre de test de mutation pour Java avec des exemples compréhensibles. Vous comprendrez ses avantages et apprendrez comment l’intégrer à votre projet.
Commençons!
J’ai créé un projet Java simple pour démontrer l’utilisation de PIT. Le code complet réside dans mon référentiel GitHub, lié dans la section Références à la fin de l’article.
Dépendances du projet
Il s’agit d’un projet basé sur Maven. Ajoutez le plugin Pitest au pom.xml
dossier:
<plugin>
<groupId>org.pitest</groupId>
<artifactId>pitest-maven</artifactId>
<version>1.9.9</version>
</plugin>
Notez que Pitest fonctionne également avec d’autres outils de construction, tels que Gradle.
Nous utilisons JUnit comme framework de test :
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit</artifactId>
<version>5.8.2</version>
<scope>test</scope>
</dependency>
Notez que PIT doit savoir quel framework de test utiliser. C’est pourquoi nous devons fournir le pitest-junit5-plugin
dépendance au plugin :
<plugin>
<groupId>org.pitest</groupId>
<artifactId>pitest-maven</artifactId>
<version>1.9.9</version>
<dependencies>
<dependency>
<groupId>org.pitest</groupId>
<artifactId>pitest-junit5-plugin</artifactId>
<version>1.1.0</version>
</dependency>
</dependencies>
</plugin>
Rapport de couverture standard
Tout d’abord, regardez un exemple simple pour comprendre la différence entre la couverture des lignées et celle des mutations.
Considérez le code suivant :
public static boolean isIsogram(String input) {
String[] splitString = input.split("");
Set<String> set = new HashSet<>(Arrays.asList(splitString));
return input.length() == set.size();
}
Un isogramme est un mot sans répétition de lettres. Ce code vérifie si la chaîne donnée est un isogramme.
Créons un test unitaire :
@Test
void generateNumberFromRange_shouldReturnTrue() {
var result = PitestDemo.isIsogram("chair");
System.out.println(result);
}
Notez que j’ai intentionnellement sauté la partie assertion pour vous montrer que la couverture sera verte. J’ai exécuté un rapport Jacoco et je vois les résultats suivants :
Selon l’outil, nous avons couvert toutes les lignes. Ce n’est pas vrai.
Ajoutons quelques assertions pour créer un test plus valide :
@Test
void generateNumberFromRange_shouldReturnTrue() {
var result = PitestDemo.isIsogram("chair");
Assertions.assertTrue(result);
}
Ça a l’air mieux, non ? Cependant, le rapport PIT montre ces résultats :
Dans la section suivante, vous verrez comment configurer PIT et générer le rapport.
Rapport de couverture avec Pitest
La configuration de base du plugin peut ressembler à ceci :
<configuration>
<targetClasses>
<param>com.mutation.demo.util.*</param>
</targetClasses>
<targetTests>
<param>com.mutation.demo.util.*</param>
</targetTests>
</configuration>
- La
targetClasses
La propriété indique à PIT où injecter les mutations. - La
targetTests
définit les tests unitaires à exécuter.
PIT offre un riche choix de propriétés à définir. Pour vous donner une idée, vous pouvez configurer les éléments suivants :
- Quelles classes ou packages sont hors de portée (pratique lorsque vous souhaitez exclure du code tiers)
- Quel type de mutation utiliser (par exemple, inverser les instructions booléennes, supprimer les appels de méthode void, renvoyer des valeurs nulles, etc.)
- Nombre de mutations par classe
- Paramètres du rapport
Vérifiez Documentation pour plus d’informations.
PIT génère automatiquement un rapport, similaire à Jacoco. Nous pouvons voir comment nos tests ont fonctionné.
Exécutez la commande maven suivante pour exécuter les tests :
mvn test pitest:mutationCoverage
Vous devriez voir un nouveau dossier sous le répertoire cible du projet appelé pit-reports
:
Ouvrez le index.html
fichier dans votre navigateur.
Vous voyez maintenant que PIT affiche des résultats légèrement différents de ceux de Jacoco. Certains des mutants ont survécu, ce qui indique que nos tests doivent être améliorés. Vous pouvez vérifier quel type de mutants ont été créés dans la section Mutations. Ce sont les mutateurs par défaut. Si vous souhaitez en désactiver certains ou en ajouter de nouveaux, configurez le <mutators>
propriété dans le plugin. Voici une liste de tous les mutateurs possibles.
Écrivons des tests supplémentaires pour couvrir les cas manquants.
Selon le rapport, nous ne vérifions pas le résultat lorsque la méthode renvoie false.
@Test
void generateNumberFromRange_shouldReturnFalse() {
var result = PitestDemo.isIsogram("look");
Assertions.assertFalse(result);
}
Relancez les tests :
Cette fois, nous avons couvert tous les cas !
Prenons un autre exemple :
public static boolean isWithinRange(int number) {
return number <= 100 && number >= 10;
}
Cette méthode vérifie si le nombre fourni est compris entre 10 et 100.
Voici les tests JUnit :
@ParameterizedTest
@ValueSource(ints = {15, 100})
void isWithinRange_shouldReturnTrue(int input) {
var result = PitestDemo.isWithinRange(input);
Assertions.assertTrue(result);
}@ParameterizedTest
@ValueSource(ints = {105, -150})
void isWithinRange_shouldReturnFalse(int input) {
var result = PitestDemo.isWithinRange(input);
Assertions.assertFalse(result);
}
Cette fois, nous testons avec différentes entrées en utilisant le @ParameterizedTest
et @ValueSource
Jupiter annotations.
Voici le rapport standard :
Le rapport PIT ressemble à ceci :
Rapport de cas de test détaillé :
C’est presque bon. Nous n’avons pas inclus de test pour couvrir la limite inférieure, qui est 10. Ajoutons la valeur à la liste des valeurs :
@ParameterizedTest
@ValueSource(ints = {10, 15, 100})
void isWithinRange_shouldReturnTrue(int input) {
var result = PitestDemo.isWithinRange(input);
Assertions.assertTrue(result);
}
Résultat PIT :
Super, maintenant tout est couvert !
Ce tutoriel vous a appris à utiliser le plugin Pitest pour les tests de mutation. C’est bénéfique pour la qualité de vos tests unitaires.
Comme vous l’avez vu, le plugin est hautement configurable. Je vous encourage à consulter la documentation pour toutes les options afin d’en tirer le meilleur parti.
Notez que l’exécution des tests de mutation peut être longue et coûteuse. Il doit être utilisé avec prudence. Idéalement, lorsque nous modifions ou ajoutons de nouvelles classes.
J’espère que vous avez appris quelque chose de nouveau grâce à ce post. Si vous êtes intéressé par des sujets sur les tests, vous aimerez peut-être mes articles similaires :
Merci d’avoir lu, et à la prochaine !