Overblog
Editer l'article Suivre ce blog Administration + Créer mon blog
Laurent COCAULT

Discours sur une méthode

Discours sur une méthode

Le dernier numéro de GLMF, le numéro 227 de juin 2019, propose un article qui présente des outils de génération automatique de tests avec le projet STAMP. L'article aborde plusieurs stratégies, correspondant aux différents outils de STAMP, qui permettent d'amplifier les tests unitaires ou les configurations de déploiement. En lisant la partie de l'article consacrée à l'outil de test par mutation PIT et au moteur de mutation extrême Descartes, je me suis demandé ce que cette approche d'amplfication pourrait donner sur une librairie que j'ai développée, avec la conviction de l'avoir plutôt bien testée: le module "constraint" de ma librairie CSP.

Le principe de la mutation de tests consiste à exécuter les suites de tests existantes sur une version du code source dans lequel des mutations, radicales dans le cas de Descartes, ont été apportées. Par mutation radicale, on doit comprendre que la totalité du corps des fonctions testées est substituée par une fonction volontairement erronée. Par exemple une mutation "void" supprime le contenu d'une méthode sans type de retour. Une mutation "null" remplace le corps des méthodes retournant un objet par un "return null". Dans un cas comme dans l'autre, le test doit échouer auquel cas on considère que la mutation a été "tuée". Si une mutation survit, c'est qu'on peut considérer que des tests sont manquants ou que les tests sont trop laxistes pour le détecter. Comme je le répète assez souvent, une couverture de test ne doit pas être considérée comme un indicateur absolu de fiabilité du logiciel testé: pour prendre un cas extrême, un test sans assertion peut couvrir du code à 100% sans avoir vérifié son comportement. Au mieux, on se sera assuré qu'il n'y a pas de code mort ou que le code ne "plante" pas. Mais on sera passé à côté de l'intérêt même du test qui est vérifier un comportement et des résultats, sans même parler d'analyser ce comportement dans des cas particuliers, dégradés ou aux limites.

Couverture de constraint

Couverture de constraint

Le tableau ci-dessus présente la couverture des tests unitaires du module "constraint" de la suite de projets CSP dédié à la résolution de problèmes sous contraintes. Le taux de couverture n'est certes pas de 100%, mais il est assez élevé pour donner confiance dans la couverture des tests (95% d'instructions couvertes et 88% des branches).

Pour vérifier si cette suite de tests est assez solide, je me suis proposé d'y intégrer le plugin PIT. et son moteur de mutations Descartes. On commence avec un complément au fichier POM du projet.

<!-- Test amplification -->
<plugin>
    <groupId>org.pitest</groupId>
    <artifactId>pitest-maven</artifactId>
    <version>1.4.7</version>
    <configuration>
        <mutationEngine>descartes</mutationEngine>
    </configuration>
    <dependencies>
        <dependency>
            <groupId>eu.stamp-project</groupId>
            <artifactId>descartes</artifactId>
            <version>1.2.5</version>
        </dependency>
    </dependencies>
</plugin>

On poursuit avec l'exécution de la commande suivante:

mvn org.pitest:pitest-maven:mutationCoverage -DmutationEngine=descartes

L'exécution de cette commande produit un certain nombre de messages d'information et d'avertissement, dont certains mettent en évidence des timeouts d'attente. En effet, il est important de souligner que le module "constraint" de CSP est multi-threadé pour permettre la résolution de problème en tirant profit d'une infrastructure multi-coeurs. Après environ trois minutes d'attente (mon PC n'est pas du dernier cri), on obtient la synthèse suivante:

/================================================================================
- Mutators
================================================================================
> 0
>> Generated 7 Killed 7 (100%)
> KILLED 7 SURVIVED 0 TIMED_OUT 0 NON_VIABLE 0
> MEMORY_ERROR 0 NOT_STARTED 0 STARTED 0 RUN_ERROR 0
> NO_COVERAGE 0
--------------------------------------------------------------------------------
> 1
>> Generated 7 Killed 7 (100%)
> KILLED 7 SURVIVED 0 TIMED_OUT 0 NON_VIABLE 0
> MEMORY_ERROR 0 NOT_STARTED 0 STARTED 0 RUN_ERROR 0
> NO_COVERAGE 0
--------------------------------------------------------------------------------
> void
>> Generated 37 Killed 31 (84%)
> KILLED 30 SURVIVED 2 TIMED_OUT 1 NON_VIABLE 0
> MEMORY_ERROR 0 NOT_STARTED 0 STARTED 0 RUN_ERROR 0
> NO_COVERAGE 4
--------------------------------------------------------------------------------
> null
>> Generated 42 Killed 39 (93%)
> KILLED 38 SURVIVED 1 TIMED_OUT 1 NON_VIABLE 0
> MEMORY_ERROR 0 NOT_STARTED 0 STARTED 0 RUN_ERROR 0
> NO_COVERAGE 2
--------------------------------------------------------------------------------
> false
>> Generated 51 Killed 50 (98%)
> KILLED 49 SURVIVED 1 TIMED_OUT 1 NON_VIABLE 0
> MEMORY_ERROR 0 NOT_STARTED 0 STARTED 0 RUN_ERROR 0
> NO_COVERAGE 0
--------------------------------------------------------------------------------
> true
>> Generated 51 Killed 50 (98%)
> KILLED 49 SURVIVED 1 TIMED_OUT 1 NON_VIABLE 0
> MEMORY_ERROR 0 NOT_STARTED 0 STARTED 0 RUN_ERROR 0
> NO_COVERAGE 0
--------------------------------------------------------------------------------
================================================================================
- Timings
================================================================================
> scan classpath : < 1 second
> coverage and dependency analysis : 7 seconds
> build mutation tests : 1 seconds
> run mutation analysis : 2 minutes and 25 seconds
--------------------------------------------------------------------------------
> Total  : 2 minutes and 35 seconds
--------------------------------------------------------------------------------
================================================================================
- Statistics
================================================================================
>> Generated 195 mutations Killed 184 (94%)
>> Ran 232 tests (1.19 tests per mutation)
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time:  02:48 min
[INFO] Finished at: 2019-06-09T15:49:39+02:00
[INFO] ------------------------------------------------------------------------

On peut également constater qu'un rapport a été produit dans le dossier "target/pitreports", portant pour nom la date d'exécution. Le rapport ainsi produit est le suivant.

Discours sur une méthode

Le rapport de couverture à proprement parler n'est pas très différent de celui produit à partir du code original. Il est en revanche intéressant de s'attacher aux mutations qui ont survécu pour identifier une insuffisance des tests.

Sur le modèle de base des contraintes, on identifie que 97 des 98 mutations opérées ont été éliminées par la suite de test. Le rapport produit par PIT permet d'aller consulter la mutation ayant survécu. Elle concerne la classe "Interval" qui permet de représenter un intervalle de valeurs possible dans le domain d'une variable traitée par le moteur CSP, plus particulièrement la méthode "isEmpty" qui permet de savoir si l'intervalle est vide.

Discours sur une méthode
Discours sur une méthode

Le cas de figure rentré n'est pas inintéressant dans la mesure où son analyse révèle plusieurs problèmes:

  • le premier problème est que la classe Interval ne fait pas l'objet d'un test dédié mais est testé au travers des tests unitaires de la classe IntervalDomain. Cette approche rend difficile la lecture des cas de figure qui conduisent finalement à ne tester que les cas où la fonction "isEmpty" retourne "false". L'élimination de cette lacune consiste donc à ajouter une suite de test dédiée à la classe Interval qui permette de vérifier que "isEmpty" peut retourner une valeur "true" ou une valeur "false".
  • La mise en oeuvre de ce test permet de détecter un second problème: la méthode "isEmpty" ne peut jamais retourner "false" pour un intervalle de valeurs entières. On s'appuie en effet pour cela sur un calcul de distance (donc un nombre positif ou nul) auquel on ajoute "1" pour représenter le fait que, par convention, les intervalles sont ouverts à droite et fermés à gauche. L'implémentation de la méthode "isEmpty" est donc impropre et doit être remplacer par une comparaison des bornes de l'intervalle.

Ce qui est "in fine" un peu décevant avec cette analyse, c'est qu'on aurait pu la mener avec l'analyse de couverture initiale: la méthode "isEmpty" était couverte à 100% en instruction mais à 50% en branche (seul le cas du retour "false" était testé).

Une analyse comparée de la couverture des tests unitaires et du rapport PIT montre que de nombreuses mutations survivent à cause de branches de code non couvertes. On peut néanmoins identifier quelques mutants qui survivent alors même que la couverture est annoncée complète en instructions et en branches. C'est le cas de la fonction "render" du SolverDotRenderer dont le propos est de restituer un arbre au format DOT de GraphViz. Dans ce cas in peut en effet constater que le résultat de la mise en forme est affiché sur la sortie standard mais n'est pas comparée avec un résultat de référence.

En conclusion, la mise en oeuvre de PIT et de Descartes est assez décevante. Une analyse consciencieuse des défauts de couverture de la suite de tests permet d'identifier l'essentiel des insuffisances de test. Cet outillage complémentaire présente pour intérêt principal de détecter l'absence d'assertions dans une batterie de tests. Mais des dispositions méthodologiques consistant à appliquer rigoureusement un GIVEN-WHEN-THEN voire à dérouler une approche TDD doivent permettre d'atteindre le meilleur résultat en amont.

Partager cet article
Repost0
Pour être informé des derniers articles, inscrivez vous :
Commenter cet article
A
Merci pour cet article très intéressant. De mon côté, je l'ai utilisé dans le contexte d'un projet qui avait une faible couverture de tests afin de trouver un grand nombre de tests à réaliser pour tuer un mutant survivant. Je m'étais dit que cela serait un exercice pédagogique intéressant à mener pour un débutant en test. Le but aurait été de lui faire prendre conscience de manière pratique de ce que l'on doit tester en TU. Le hic, le rapport généré (j'ai utilisé infection PHP) est volumineux et contient beaucoup de faux positif. Ce qui est déroutant pour un débutant.
Répondre