Test de mutation avec PITest

1. Vue d’ensemble

Les tests logiciels font référence aux techniques utilisées pour évaluer la fonctionnalité d’une application logicielle. Dans cet article, nous allons discuter de certaines mesures utilisées dans l’industrie du test de logiciel, telles que couverture de code et test de mutation , avec un intérêt particulier sur la façon d’effectuer un test de mutation à l’aide de http://pitest . .org/[ bibliothèque PITest ].

Par souci de simplicité, nous allons fonder cette démonstration sur une fonction palindrome de base - Notez qu’un palindrome est une chaîne qui lit les mêmes en arrière et en avant.

2. Dépendances Maven

Comme vous pouvez le voir dans la configuration des dépendances Maven, nous utiliserons JUnit pour exécuter nos tests et la bibliothèque PITest pour introduire des mutants dans notre code - ne vous inquiétez pas, nous verrons dans un instant ce qu’il est.

Vous pouvez toujours rechercher la dernière version de dépendance par rapport au référentiel central maven en suivant ce lien https://search.maven.org/classic/#search%7Cga%7Ca%7Ca%3A%22Active-parent%22 .

<dependency>
    <groupId>org.pitest</groupId>
    <artifactId>pitest-parent</artifactId>
    <version>1.1.10</version>
    <type>pom</type>
</dependency>

Pour que la bibliothèque PITest soit opérationnelle, nous devons également inclure le plugin pitest-maven dans notre fichier de configuration pom.xml :

<plugin>
    <groupId>org.pitest</groupId>
    <artifactId>pitest-maven</artifactId>
    <version>1.1.10</version>
    <configuration>
        <targetClasses>
            <param>com.baeldung.testing.mutation.** </param>
        </targetClasses>
        <targetTests>
            <param>com.baeldung.mutation.test.** </param>
    </targetTests>
     </configuration>
</plugin>

3. Configuration du projet

Maintenant que nos dépendances Maven sont configurées, examinons cette fonction palindrome explicite:

public boolean isPalindrome(String inputString) {
    if (inputString.length() == 0) {
        return true;
    } else {
        char firstChar = inputString.charAt(0);
        char lastChar = inputString.charAt(inputString.length() - 1);
        String mid = inputString.substring(1, inputString.length() - 1);
        return (firstChar == lastChar) && isPalindrome(mid);
    }
}

Tout ce dont nous avons besoin maintenant, c’est d’un simple test JUnit pour nous assurer que notre implémentation fonctionne de la manière souhaitée:

@Test
public void whenPalindrom__thenAccept() {
    Palindrome palindromeTester = new Palindrome();
    assertTrue(palindromeTester.isPalindrome("noon"));
}

Jusqu’ici tout va bien, nous sommes prêts à exécuter notre scénario de test avec succès en tant que test JUnit.

Ensuite, dans cet article, nous allons nous concentrer sur la couverture de code et de mutations à l’aide de la bibliothèque PITest.

4. Couverture de code

La couverture de code a été largement utilisée dans l’industrie du logiciel pour mesurer le pourcentage des chemins d’exécution qui ont été exercés au cours de tests automatisés.

Nous pouvons mesurer la couverture de code effective en fonction des chemins d’exécution à l’aide d’outils tels que Eclemma disponible sur Eclipse IDE.

Après avoir exécuté TestPalindrome avec une couverture de code, nous pouvons facilement atteindre un score de couverture de 100%. Notez que isPalindrome est récursif. Il est donc tout à fait évident que le contrôle de la longueur d’entrée vide sera couvert de toute façon.

Malheureusement, les métriques de couverture de code peuvent parfois être assez inefficaces , car un score de couverture de code de 100% signifie seulement que toutes les lignes ont été exercées au moins une fois, mais cela ne dit rien sur l’exactitude des tests ou la complétude des cas d’utilisation , et c’est pourquoi le test de mutation est réellement important.

5. Couverture de mutation

Le test de mutation est une technique de test utilisée pour améliorer l’adéquation des tests et identifier les défauts du code. L’idée est de changer le code de production de manière dynamique et de provoquer l’échec des tests.

Les bons tests doivent échouer

Chaque modification du code s’appelle un mutant et entraîne une version modifiée du programme, appelée mutation .

Nous disons que la mutation est tuée si elle peut causer un échec aux tests. Nous disons également que la mutation a survécu si le mutant ne pouvait pas affecter le comportement des tests.

Exécutons maintenant le test avec Maven, l’option objectif étant définie sur:

org.pitest: pitest-maven: mutationCoverage .

Nous pouvons vérifier les rapports au format HTML dans le répertoire target/pit-test/YYYYMMDDHHMI :

  • Couverture en ligne 100%: 7/7

  • Couverture de mutation de 63%: 5/8

Il est clair que notre test parcourt tous les chemins d’exécution, le score de couverture de ligne est donc de 100%. D’autre part, la bibliothèque PITest a introduit 8 mutants , 5 d’entre eux ont été tués - Causé un échec - mais 3 ont survécu.

Nous pouvons consulter le rapport com.baeldung.testing.mutation/Palindrome.java.html pour plus de détails sur les mutants créés:



Ce sont les mutateurs actifs par défaut lors de l’exécution d’un test de couverture de mutation:

  • INCREMENTS MUTATOR__

  • VOID METHOD CALL MUTATOR__

  • RETURN VALS MUTATOR

  • MATH MUTATOR__

  • NEGATE CONDITIONALS MUTATOR

  • INVERT NEGS MUTATOR

  • CONDITIONALS BOUNDARY MUTATOR

Pour plus de détails sur les mutateurs PITest, vous pouvez consulter le lien officiel page de documentation ** .

Notre score de couverture de mutations reflète le manque de cas de test , car nous ne pouvons pas nous assurer que notre fonction palindrome rejette les entrées de chaînes non palindromiques et quasi palindromiques.

6. Améliorer le score de mutation

Maintenant que nous savons ce qu’est une mutation, nous devons améliorer notre score de mutation en tuant les mutants survivants .

Prenons comme exemple la première mutation - annulée conditionnelle - de la ligne 6. Le mutant a survécu parce que même si nous modifions l’extrait de code:

if (inputString.length() == 0) {
    return true;
}

À:

if (inputString.length() != 0) {
    return true;
}

Le test réussira et c’est la raison pour laquelle la mutation a survécu . L’idée est de mettre en œuvre un nouveau test qui échouera si le mutant est introduit . La même chose peut être faite pour les mutants restants.

@Test
public void whenNotPalindrom__thanReject() {
    Palindrome palindromeTester = new Palindrome();
    assertFalse(palindromeTester.isPalindrome("box"));
}
@Test
public void whenNearPalindrom__thanReject() {
    Palindrome palindromeTester = new Palindrome();
    assertFalse(palindromeTester.isPalindrome("neon"));
}

Maintenant, nous pouvons exécuter nos tests en utilisant le plugin de couverture de mutation, pour nous assurer que toutes les mutations ont été tuées , comme on peut le voir dans le rapport PITest généré dans le répertoire cible.

  • Couverture en ligne 100%: 7/7

  • Couverture de mutation à 100%: 8/8

7. Configuration des tests PITest

Les tests de mutation peuvent parfois nécessiter beaucoup de ressources. Nous devons donc mettre en place une configuration appropriée pour améliorer l’efficacité des tests. Nous pouvons utiliser la balise targetClasses pour définir la liste des classes à muter. Le test de mutation ne peut pas être appliqué à toutes les classes d’un projet réel, car cela prendra du temps et des ressources.

Il est également important de définir les mutateurs que vous prévoyez d’utiliser lors du test de mutation, afin de minimiser les ressources informatiques nécessaires pour effectuer les tests:

<configuration>
    <targetClasses>
        <param>com.baeldung.testing.mutation.** </param>
    </targetClasses>
    <targetTests>
        <param>com.baeldung.mutation.test.** </param>
    </targetTests>
    <mutators>
        <mutator>CONSTRUCTOR__CALLS</mutator>
        <mutator>VOID__METHOD__CALLS</mutator>
        <mutator>RETURN__VALS</mutator>
        <mutator>NON__VOID__METHOD__CALLS</mutator>
    </mutators>
</configuration>

De plus, la bibliothèque PITest offre diverses options pour personnaliser vos stratégies de test . Vous pouvez spécifier le nombre maximal de mutants introduits par une classe à l’aide de l’option maxMutationsPerClass , par exemple. Plus de détails sur les options PITest dans le officiel Guide de démarrage rapide Maven .

8. Conclusion

Notez que la couverture de code est toujours une métrique importante, mais parfois, elle n’est pas suffisante pour garantir un code bien testé. Ainsi, dans cet article, nous avons décrit le test de mutation comme moyen plus sophistiqué d’assurer la qualité des tests et d’approuver les cas de test, à l’aide de la bibliothèque PITest .

Nous avons également vu comment analyser un rapport de base PITest tout en améliorant le score de couverture de mutation .

Même si les tests de mutation révèlent des défauts de code, il convient de l’utiliser à bon escient, car il s’agit d’un processus extrêmement coûteux et long .

Vous pouvez consulter les exemples fournis dans cet article dans le projet GitHub .