Accueil > Java EE > PIT : Une autre approche des tests unitaires

PIT : Une autre approche des tests unitaires

A l’occasion de la conférence Devoxx France édition 2014, une présentation a particulièrement retenu mon attention. Il s’agit de celle de PIT (Parallel Isolated Test), un outil de tests unitaires s’inscrivant dans les pratiques Test Driven Development (développement piloté par les tests) consistant à écrire les tests unitaires avant d’écrire le code source d’un logiciel. Ce sujet a été présenté durant la conférence sous forme d’un quickie d’une durée de quinze minutes par Alexandre Victoor, architecte technique à la Société Générale et ancien novédien.

PIT part du principe que le fait d’avoir une couverture de test des lignes ou des branches de code approchant les 100% ne garantit pas la qualité des tests. En effet, une couverture de tests nous informe uniquement sur ce qui n’est pas du tout testé mais pas sur la pertinence des tests. A l’inverse, PIT se veut qualitatif, plutôt que quantitatif, en essayant de mettre nos tests unitaires en défaut.

PIT permet d’introduire des défauts positifs appelés mutants (il s’agit de fragments de code qui sont censés faire échouer les tests) dans le code applicatif afin de voir si les tests initialement réalisés les détectent. Si à l’issue de l’introduction de ces mutants aucun des tests n’échoue, cela indique que les tests effectués ne sont pas pertinents. Dans le cas contraire, si l’un des tests échoue, on dit que le mutant introduit a été tué. On peut ainsi mesurer la qualité des tests en mettant en rapport le nombre de mutants introduits avec le nombre d’entre eux qui ont été « tués ».

Les avantages de PIT sont qu’il est Open Source, rapide à exécuter, et qu’il s’intègre dans les principaux outils de structuration de builds tels que ANT, Maven ou encore Gradle.

PIT nécessite au minimum Java 5 et fonctionne à partir de JUnit 4 ou TestNG 6.

 

Démarrer avec PIT

Pour démarrer avec PIT, si l’on utilise Maven, il faut rajouter une dépendance de la façon suivante :

<dependency>
  <groupId>org.pitest</groupId>
  <artifactId>pitest-maven</artifactId>
  <version>1.0.0</version>
</dependency>

On peut également préciser dans Maven les packages dans lesquels on autorise PIT à injecter des mutants. En d’autres termes les parties du code que l’on souhaite tester. Il est aussi possible d’indiquer le package dans lequel se trouvent les tests qui vont servir à éliminer les mutants.

Ce paramétrage est important car il permet d’une part de ne pas travailler sur une partie de code qui ne le nécessite pas et d’autre part d’exclure les tests non pertinents. Typiquement, l’exécution de tests de performances ne présente pas un réel intérêt dans le cadre de PIT.

Ci-dessous, un exemple de paramétrage :

<configuration>
  <targetClasses>
    <param>com.projet.sources.*</param>
  </targetClasses>
  <targetTests>
    <param>com.tests.unitaires</param>
  </targetTests>
</configuration>

Il faut ensuite lancer en ligne de commande l’exécution de PIT en tapant la commande suivante : mvn org.pitest:pitest-maven:mutationCoverage

Pour une commande de type mvn clean install org.pitest:pitest-maven:mutationCoverage, PIT s’exécute après le build du projet, l’exécution des  tests unitaires et la génération de l’archive.

La console va afficher les résultats mais un rapport au format HTML est également disponible dans le répertoire target du projet. Ce rapport présente le nombre de mutants tués par classes et par packages en rapport avec le nombre de lignes de code.

 

Un exemple d’utilisation de PIT

Soit un projet nommé pitest contenant deux classes nommées Main.java et Utilitaire.java contenues dans le package com.projet.sources :

  • La classe Utilitaire.java contient une méthode retournant un entier déterminé à l’avance
  • La classe Main.java se contente d’appeler la méthode de la classe Utilitaire.java

Ce projet contient également une classe de test unitaire nommée UtilitaireTest.java se trouvant dans le package com.projet.test. La classe de test UtilitaireTest.java contient deux méthodes de test de la classe  Utilitaire.java.

Ci-dessous, le code de la classe Utilitaire.java :

package com.projet.sources;

/**
* @author a.dalmeida@novediagroup.com
*
*/
public class Utilitaire {

  public static int sayHello() {
    int indiceMax = 4;
    int variable = 0;

    for(int i = 0; i<indiceMax; i++) {
      if (i>= 3) {
        System.out.println("Hello world!");
      } else {
        variable++;
      }
    }

    System.out.println("variable " + variable);

    return indiceMax;
  }
}

Ci-dessous, un exemple de rapport généré par PIT après exécution sur  les sources contenues dans le package com.projet.sources :

PIT Coverage Report

Line Coverage : Pourcentage de lignes couvertes par les mutations (nombre de lignes mutées/nombre de lignes de code totales).
Mutation Coverage : Pourcentage de mutants tués (nombre de mutants tués/nombre de mutants introduits).

Il est possible de zoomer dans chaque classes pour savoir sur quelles lignes des mutants ont été insérés et s’ils ont été tués ou non. Pour cela il faut cliquer sur le lien associé au package des classes sources.

MutationCoverage

PIT nous indique que 13 mutants ont été introduits. 4 dans la  classe Main.java et 9 dans la classe Utilitaire.java.

En cliquant sur le lien Utilitaire.java, chaque ligne. La ligne concernée est surlignée en rouge si le mutant introduit a survécu et en vert foncé s’il a été tué.

UtilitaireJavaCoverage

 

MutationCoverage

Dans la section Mutations du rapport, on a le détail des types de mutation introduits en fonction des lignes ainsi que le résultat : SURVIVED en cas de survie et KILLED en cas de destruction.

La section Active mutators liste les types de mutation introduits.

Enfin la section Tests examined récapitule les tests unitaires analysés.

Outre le HTML, il est possible d’obtenir le rapport sous un autre format. On peut paramétrer PIT de sorte à obtenir un fichier au format CSV ou XML.

 

Fonctionnement de PIT

PIT introduit les mutants dans le code à tester en utilisant des opérateurs de mutation, en supprimant des appels à des méthodes, en intervertissant des opérateurs logiques, ou encore en modifiant des valeurs de retour de certaines fonctions.

PIT fournit une dizaine d’opérateurs de mutation dont sept sont activés par défaut. Les mutations sont effectuées directement sur les binaires (fichier .class) issus de la compilation du code.

Seuls les tests qui couvrent les mutations introduites sont lancés. Pour ce faire, PIT effectue au préalable, une analyse de la couverture de tests puis utilise ces résultats afin de  cibler les tests à lancer pour tuer les mutants. Ainsi, les classes disposant d’un bon taux de couverture de tests seront traitées en priorité.

Malheureusement, on peut rencontrer des limitations dans le cadre de la génération des mutations. En effet, tous les mutants n’ont pas un comportement différent des classes non mutées dont ils proviennent. On parle alors de mutants équivalents.

L’exemple ci-dessous nous permet de l’illustrer :

Le code :

int i=2;
if(i>=1){
  return "Hello world!";
}

Peut être muté en :

int i=2;
if(i>1){
  return "Hello world!";
}

Les 2 codes ci-dessus produisent le même comportement et par conséquent, les tests unitaires ne sont pas censés échouer sur le second code plutôt que sur le premier.

On parle aussi de mutations équivalentes lorsque les mutants sont générés sur du code non fonctionnel. Elles concernent essentiellement l’appel à des classes de frameworks de logs. Ce problème peut être évité en fournissant à PIT ces classes. De plus, de base, PIT exclu des mutations les appels aux principaux frameworks de logs.

Lorsque l’on travaille dans un environnement d’intégration continue et que l’on est amené à jouer des tests unitaires après chaque build, PIT peut faire de l’analyse incrémentale. Elle consiste à n’appliquer des mutations que sur la partie du code qui a changé depuis la dernière exécution des tests. Cette option permet de gagner du temps puisque PIT se limitera à lancer les tests couvrant les parties de code ayant subi une modification.

 

Les opérateurs de mutation

Intéressons nous aux principaux opérateurs de mutation utilisés par PIT.

Les opérateurs de limites conditionnelles :

Ils remplacent les opérateurs relationnels. Chaque opérateur est remplacé par un autre suivant le tableau ci-dessous :

Opérateur original Opérateur muté
< <=
<= <
> >=
>= >

Les mutations de négation de condition :

Cette mutation consiste à remplacer tous les opérateurs logiques par leur contraire. Par exemple, l’opérateur d’égalité (==) sera remplacé par l’opérateur de différence (!=) et celui d’infériorité (<) par supérieur ou égal (>=).

Les mutations de suppression de conditions :

Cette mutation consiste à remplacer toutes les instructions conditionnelles afin de faire en sorte qu’une condition soit toujours vraie.

Par exemple, la condition ci-dessous :

if (x == y) {
  ...
}

est mutée en :

if (true) {
  ...
}

Les mutations mathématiques :

Les mutations mathématiques consistent à remplacer les opérations arithmétiques binaires entre des entiers ou des flottants par d’autres opérations.

Le tableau ci-dessous regroupe un exemple.

Opérateur initial Opérateur muté
+
+
* /
/ *
% *
& |
| &
^ &

 

Les mutations de non appel à des méthodes void :

Ce type de mutation consiste à supprimer tous les appels aux méthodes renvoyant void. Il faut noter que ceci ne concerne pas les constructeurs.

Par exemple, le code :

public Object test() {
  Object o = new Object ();
  return o;
}

Sera remplacé par :

public Object test() {
  Object o = null;
  return o;
}

 

PIT et la qualimétrie

PIT permettant d’avoir un feedback sur la qualité des tests unitaires, il peut être tentant de les intégrer dans des rapports de qualimétrie. Pour ce faire, il existe un plugin Pit+Sonarqube développé par Alexandre permettant de faire figurer les résultats de PIT dans les rapports de Sonar. Ce plugin permet en plus de suivre dans Sonar le Mutations coverage. Il s’agit du pourcentage de mutants tués par rapport au nombre de mutants introduits.
Ce ratio est en fait le pourcentage de chance que l’on a de détecter grâce à nos tests unitaires une anomalie introduite dans le code.

 

Les autres outils de tests par mutation

PIT n’est pas le seul outil de mutation testing. Il en existe d’autres tels que Jester, Jumble, µJava et JavaLanche, Ils ont chacun leurs avantages et leurs inconvénients allant des versions de Java supportées, à la rapidité d’exécution mais aussi à leur capacité d’intégration au sein de gestionnaires de builds. Mais dans cette liste PIT, Jumble et JavaLanche sont les options les plus sérieuses à considérer pour une utilisation au sein d’un projet d’envergure.

Bien que PIT soit le plus récent des trois outils de tests précédemment cités, il est celui qui bénéficie des releases les plus fréquentes, ce qui le rend beaucoup plus mature que les autres. En mai 2014, une release majeure a été effectuée faisant ainsi passer le logiciel de la version 0.33 à la 1.0.0. Ce qui nous permet de confirmer sa maturité.

 

Conclusion

Les tests par mutation permettent d’obtenir un degré de fiabilité des tests beaucoup plus pertinent que des métriques comme la couverture du code par les tests. PIT est une solution simple, facile à prendre en main, nous permettant d’éprouver nos tests unitaires. De plus, les nombreuses options dont dispose l’outil le rendent adaptable à tous types de projets.

Il reste néanmoins à définir le ratio mutants introduits/mutants tués le plus pertinent pour son projet.

 

Sources

Categories: Java EE Tags: , ,
  1. Pas encore de commentaire
  1. Pas encore de trackbacks


6 × sept =