Il y a 11 années -

Temps de lecture 10 minutes

L’analyse de couverture de code en Java

Il ne reste plus grand’monde pour soutenir que l’écriture de tests unitaires automatisés est une perte de temps sur un projet logiciel – la notion de dette technique entre dans les moeurs. Cette prise de conscience salutaire se heurte pourtant souvent à deux grandes catégories de difficultés :

  • Le conservatisme – pour ne pas dire la stupidité – de certains chefs de projet, qui persistent à voir dans l’écriture des tests une activité contre-productive, qui servira de variable d’ajustement au moindre coup de grisou
  • L’existant : quand le projet n’a pas systématisé la pratique du test dès son origine et que la multiplication des anomalies tardives le pousse à la réintroduire. Par où commencer ?

Pour le premier point, la solution passe par un effort pédagogique, ou, pour les cas désespérés, par une reconversion imposée ou un congé sabbatique.
Pour le second, il faut un peu de bon sens, et un peu d’outillage.

C’est sur le deuxième aspect qu’interviennent les outils d’analyse de couverture de code (ou « code coverage » en anglais). Dans la suite, nous verrons que ces outils ne permettent pas d’évaluer les tests unitaires d’un point de vue qualitatif, mais qu’ils peuvent en revanche apporter au bon sens un précieux appui en répondant aux questions suivantes :

  • Quels sont les tests unitaires déjà en place ?
  • Les tests sont-ils en phase (à jour) avec le code qu’ils testent ?
  • Les fonctions critiques de l’application sont-elles couvertes par les tests ?

Quelques définitions

Pour rappel, on distingue 2 grandes catégories complémentaires de tests pour un projet logiciel :

  • Test par « boîte noire » (black box testing) : les tests sont effectués sans connaissance de la structure interne et des objets manipulés par le système. On ne s’intéresse qu’aux entrées et sorties de ce système. Les tests fonctionnels ou les tests manuels par exemple, appartiennent à cette catégorie.
  • Test par « boîte blanche » (white box testing) : les tests s’intéressent au fonctionnement interne du système. Ainsi, les tests unitaires manipulent les objets et éléments qui composent le système. L’analyse de couverture de code permet d’obtenir une mesure indirecte de la complétude des tests effectués à ce niveau.

La couverture de code est une mesure qui décrit le degré auquel le code source d’un programme a été testé. Ainsi c’est l’exécution de tous les tests (unitaires, fonctionnels, etc…) qui va permettre de déterminer notre mesure de couverture.

Les outils fournissant cette mesure savent déterminer quels fragments de code ou quelles méthodes ont été exécutés. La mesure (ou « métrique« ) principale est calculée en faisant le rapport du nombre de lignes de code exécutées sur le nombre de lignes total du code source : c’est la couverture de ligne (« statement coverage« ).

Plusieurs autres métriques (ou « critères« ) existent et permettent de calculer la couverture de code à différents niveau de granularité :

  • Method coverage : Est-ce que toutes les méthodes ont été exécutées ?
  • Branch coverage : Est-ce que les structures de contrôle (if ou while par exemple) ont été testées pour les 2 cas vrai et faux ?
  • Path coverage : Est-ce que tous les chemins possibles d’exécution des fonctions d’un programme ont été exécutés ?

Les stratégies d’instrumentation

En Java, il existe plusieurs manières de déterminer quelles parties du code ont été exécutées.

stratégies d’instrumentation

On distingue 2 techniques :

  • L’instrumentation des classes :
       – L’outil d’instrumentation ajoute des lignes dans le code source du programme lui permettant de détecter le code exécuté.
       – L’outil intervient directement sur le code compilé (byte-code) et ajoute ses modifications (« tracking hooks« ).
  • L’utilisation des options de la JVM : il est possible d’utiliser une configuration particulière de la JVM pour utiliser des « listeners » (utilisant les interfaces JVMPI ou JVMDI, remplacés par JVMTI depuis Java 5). Ainsi certains outils de profiling utilisent un environnement d’exécution particulier permettant de détecter le code exécuté.

Les outils les plus courants utilisent la technique d’instrumentation des classes, le plus souvent au niveau byte-code. Les raisons principales en sont les suivantes :

  • La surcharge à l’exécution (« runtime overhead« ) pour les classes instrumentées est comprise entre 5 et 20%. L’utilisation de profilers JVM est beaucoup plus pénalisante.
  • Pas besoin du code source pour instrumenter.
  • Instrumentation des jars tiers possible.
  • Indépendant de la plateforme.
  • Facilité de branchement de l’outil ad-hoc.

A noter que certains outils utilisent une approche complémentaire : l’instrumentation dynamique (au runtime). Par exemple Emma permet d’instrumenter les classes à la volée (on the fly) et avec l’utilisation d’un classloader modifié, d’obtenir les informations de couverture d’un programme en cours d’exécution. Hansel encapsule les tests unitaires (JUnit par exemple) dans des classes spécifiques permettant d’injecter à l’exécution (à l’aide d’un classloader modifié) le code permettant de tester la couverture.

Exemple d’utilisation avec l’outil Cobertura

Caractéristiques

  • Le projet Cobertura est basé sur JCoverage :
       – Il est gratuit (licence Apache et GPL).
       – Utilise l’instrumentation du byte-code en mode déconnecté (après compilation).
  • Génération de rapports en HTML ou XML :
       – Possibilité de fusion de différents rapports de couverture (merge).
       – Possibilité d’incorporer les rapports dans les outils d’intégration continue (Hudson, Cruise Control).
  • Metrics :
       – Statement et branch coverage pour les classes, packages et l’ensemble d’un projet.
       – Affichage de la complexité cyclomatique du code de McCabe pour chaque classes, moyenne pour chaque package et globale pour l’ensemble du projet.
  • Intégration possible en ligne de commande, dans une tâche Ant ou avec Maven.

Exemple de tâches Ant

Instrumentation des classes compilées

Cette tâche permet d’instrumenter les classes :

<cobertura-instrument todir="${instrumented.dir}">
    <ignore regex="org.apache.log4j.*" />
    <fileset dir="${classes.dir}">
        <include name="**/ui/**/*.class" />
        <exclude name="**/*Test.class" />
    </fileset>
    <fileset dir="${jars.dir}">
        <include name="domain.jar" />
    </fileset>
</cobertura-instrument>
Tips & Tricks :

  • <ignore regex="org.apache.log4j.*" /> : cette ligne permet d’ignorer tous les appels à toutes les méthodes des classes qui correspondent à l’expression régulière. Toutefois les classes correspondantes seront instrumentées.
  • On peut utiliser les filesets Ant standards pour inclure ou exclure certains packages ou classes du chemin d’instrumentation. De la même manière on peut instrumenter des jars.

Cobertura peut également instrumenter des archives incluses dans d’autres archives. Par exemple, des jars contenu dans le répertoire WEB-INF/lib d’un war seront instrumenté et le war sera reconstruit avec les jars instrumentés inclus.

Exécution des tests unitaires

Les parties entre [ ] sont à ajouter/modifier dans une tâche Ant JUnit classique :

<junit [fork="yes"] dir="${basedir}" failureProperty="test.failed">
	[<sysproperty key="net.sourceforge.cobertura.datafile" file="${basedir}/cobertura.ser" />]
	[<classpath location="${instrumented.dir}" />]
	<classpath location="${classes.dir}" />
	[<classpath refid="cobertura_classpath" />]
	...
</junit>
Tips & Tricks :

  • Il est nécessaire de spécifier la valeur « yes » pour le mode fork de la tâche JUnit pour qu’elle soit exécutée dans une JVM différente de celle de Ant. En effet, Cobertura n’écrit les données de couverture que lorsque la JVM a terminé son exécution. Or on a besoin que ces données soit écrite avant que Ant ne termine car il faut générer le rapport Cobertura.
  • Il faut déclarer le chemin vers le fichier dans lequel seront stockées les informations de couverture (ici cobertura.ser).
  • Ajouter le chemin vers le répertoire contenant les classes instrumentées. Il est important que la déclaration de ce chemin soit placée avant celui du classpath vers les classes non instrumentées.
  • Il faut ensuite ajouter dans le classpath les jars nécessaires à l’exécution de Cobertura (cobertura.jar + librairies asm + jakarta oro)

Dans le cas d’utilisation de Cobertura avec un serveur d’application les données de couverture de code ne seront écrites dans le fichier que lorsque le serveur sera arrêté. Si cela pose un problème, il est possible d’ajouter dans le code (à la fin des tests unitaires par exemple) un appel à la méthode net.sourceforge.cobertura.coveragedata.ProjectData.saveGlobalProjectData() qui forcera l’écriture.

Génération du rapport

Pour générer le rapport au format HTML on utilisera le code suivant :

<cobertura-report format="html" destdir="${coveragereport.dir}" srcdir="${src.dir}" />
Tips & Tricks :
L’attribut « format » permet de spécifier 2 types de format en sortie : XML ou HTML. Le format XML est utile notamment pour l’intégration à Hudson.

Un exemple de rapport HTML obtenu :

Exemple de rapport HTML

Et le détail des lignes couvertes par les tests dans le code source :

Détail des lignes couvertes par les tests dans le code source

Autres fonctionnalités

Cobertura propose également une tâche, « cobertura-check », permettant de définir des seuils et de vérifier comment le code est couvert par rapport à ces seuils.

La tâche « cobertura-merge », permet de fusionner plusieurs fichiers de couverture. On peut ainsi générer des rapports globaux pour les parties client et serveur d’une application ou fusionner des données obtenues sur plusieurs sessions d’utilisation de Cobertura.

Pour chacune des opérations offertes Cobertura (instrument, report, merge, check), on peut effectuer l’appel via la ligne de commande. Par exemple :

java -cp %COBERTURA_CLASSPATH% net.sourceforge.cobertura.reporting.Main --format html --destination C:\Reports\coverage

Enfin 2 projets annexes permettent également d’intégrer Cobertura dans une configuration Maven (1 ou 2) en offrant les mêmes fonctionnalités.

Cobertura ne propose malheureusement pas de plugin d’intégration à Eclipse. Comme outil proposant les même fonctionnalités et l’intégration à Eclipse, on peut citer l’outil gratuit open source EclEmma qui est une extension de Emma cité un peu plus haut.

Comment bien utiliser l’analyse de couverture de code

Comme souvent dans l’ingénierie logicielle, l’utilisation d’une métrique de mesure de la qualité s’accompagne de son lot d’apôtres plus ou moins extrémistes. Le taux de couverture des tests n’échappe pas à cette règle, et l’on voit certains définir des objectifs quantitatifs rigides dans ce domaine. Certains affirment que le taux de couverture doit être de 100% ; d’autres, plus modestes, se contenteraient de 90%.

Dans la pratique il est difficile, voire impossible d’atteindre 100% et sur un projet conséquent ce n’est même pas pertinent de vouloir le faire. La valeur du taux de couverture n’est pas en soit une mesure pertinente de la qualité des tests.
En effet, Il est important que les équipes chargées du développement ne se focalisent pas sur un chiffre à atteindre mais plutôt sur la qualité des tests effectués. Ainsi, grâce aux informations de couverture de code, les équipes peuvent repérer et corriger rapidement les tests critiques et ne pas s’attarder à optimiser les tests concernant des parties du code moins importantes.

Brian Marick, dans « How to Misuse Code Coverage » explique de manière très claire comment bien utiliser la couverture de code dans un projet TDD (Test Driven Development). Je ne vais pas reprendre ici les points abordés, mais il est important de répéter que la mesure de couverture de code ne doit pas être une fin en soi mais un moyen d’améliorer la qualité globale du code et des tests effectués pour le valider.

Liens

Commentaire

11 réponses pour " L’analyse de couverture de code en Java "

  1. Publié par , Il y a 11 années

    Merci pour ce nouvel article intéressant qui fait une bonne présentation de l’analyse de couverture de code en Java (il me semble que le terme couverture de test est plus usité).

    De notre côté, nous introduisons cette bonne pratique dans les nouveaux projets avec maven 2. Cela fonctionne très bien avec les tests unitaires dans le cas des tests de type « boîte blanche ». Par contre, c’est plus difficile concernant les tests fonctionnels (« boîte noir ») du fait que maven 2.0.x gère mal les tests d’intégrations. On espère que la version 2.1 apportera des améliorations sur ce point. Avez-vous des retours d’expériences et des bonnes pratiques sur ce point-là ?

  2. Publié par , Il y a 11 années

    Bonjour Sanlaville,

    Merci pour votre commentaire.

    Je n’ai malheureusement pas eu le temps de tester Cobertura avec Maven (mon projet actuel utilise notre bon vieux Ant). En revanche, nous utilisons Hudson pour l’intégration continue avec des tests JUnit et Selenium pour les tests fonctionnels et l’ensemble marche plutôt bien.

    Il faut juste vérifier que les classes instrumentées sont bien prises en compte lors de la construction des jars et/ou wars du projet et que la tâche de génération du rapport de couverture de code est bien lancée après que le serveur d’intégration ait été arrêté sinon les données issues de l’instrumentation ne seront pas incluses.

    J’essairai de tester Cobertura avec Maven 2 très rapidement pour vous donner quelques pistes utiles si j’en trouve.

    A bientôt.

  3. Publié par , Il y a 11 années

    « la notion de dette technique » Quel beau lapsus !

    Je ne suis pas tout à fait d’accord sur la définition du « black-box testing », notamment sur « Les tests *fonctionnels* ou les tests *manuels* par exemple, appartiennent à cette catégorie » ou bien nous n’entendons pas la même chose par « fonctionnels » et « manuels ».

    Je trouve la définition de Mc Connell (Code Complete – Microsoft Press) plus juste : « Black-box testing refers to tests in which the tester cannot see the inner workings of the item being tested ». Je remplace volontier « item » par « method » et nous sommes bien loin du « système » tel que je l’entend. Peut-être que par fonctionnel vous entendez que dans l’optique du « black-box testing » on ne s’intéresse qu’au quoi et pas au comment ? Par contre le « black-box testing » est tout autant industrialisé et automatisé que le « white-box testing ».

    A vous lire peut-être afin d’obtenir des précisions sur ce que vous entendez par « système », « fonctionnel » et « manuel », merci.

  4. Publié par , Il y a 11 années

    Bonjour Frank,

    Tout d’abord merci pour votre commentaire.

    Pour la définition des catégories de test (boites « noire » et « blanche ») je me suis basé sur les définition fournies par Wikipedia :
    Boîte noire
    Boîte blanche

    Lorsque je parle de tests « manuels » ou « fonctionnels » je veux parler des tests où une personne physique est mise à contribution pour les exécuter manuellement. Cette personne n’a pas besoin de connaître l’implémentation ou le fonctionnement interne du système pour le tester. Il va analyser les résultats fournis par le système pour un certains nombre de cas d’utilisation.

    Effectivement certains tests fonctionnels peuvent être automatisés : je pense par exemple aux interfaces utilisateur qui peuvent être testées automatiquement à l’aide d’outils de simulation (Selenium par exemple).

    On peut aussi avoir des tests « manuels » appartenant à la catégorie « boite blanche ». Actuellement sur mon projet, certains tests sur des modules sans fil ne sont pas automatisés, et requiert une connaissance technique interne du module (mise à jour du firmware) : ce sont des tests « boîte blanche ».
    D’autres tests en revanche, ne s’interessent pas au fonctionnement interne du module mais à son comportement externe : les fonctionnalités exposées via ses interfaces (API, appels distants, UI, …) seront testées et on analysera les résultats en sortie pour les valeurs fournies en entrée. Ce sont des tests « boîte noire » car on ne connait pas le fonctionnement intrinsèque du module.

    J’ai utilisé ici le mot système car ces catégories de test sont applicables à d’autres domaines que l’informatique. Dans notre cas, on parlera d’application.

    La définition de Mc Connell est tout à fait juste, il faut juste savoir à quel niveau d’abstraction de notre système on va l’appliquer.
    Ainsi je ne suis pas d’accord sur le fait de remplacer « item » par « method » dans la définition. Les tests unitaires testent les méthodes, mais on ne teste pas individuellement chacune des lignes de code !
    En revanche, quand on utilise une API on n’est pas obligé de connaitre l’implémentation qu’il y a derrière et dans ce cas les méthodes sont en quelque sorte des boites noires.

    Pour résumer (pour le cas de l’informatique) :
    – Les tests boîte blanche testent les éléments qui composent l’application (méthodes)
    – Les tests boîte noire testent les fonctionnalités de l’application exposées via ses interfaces

    A très bientôt.

    Alexandre De Magalhaes Garcia

  5. Publié par , Il y a 11 années

    Bonjour et merci pour vos éclaircissements qui vont me permettre de détailler mon point de vue qui, en résumé, n’est pas celui d’opposer le « black-box testing » aux tests unitaires.

    Mc Connell fait cette distinction entre « black- » et « white-box testing », l’auteur précise que le « white-box testing » concerne le type de tests rédigés par le développeur (du code testé) lui-même. Je ne suis pas tout à fait d’accord, qu’il s’agisse du même développeur ou non, des tests peuvent être rédigés tenant compte ou non de l’implémentation, du corps de la méthode, lequel à mes yeux représente cette fameuse boîte. Cependant la rédaction de « black-box tests » sur une méthode (et sa classe) nécessite d’avoir définit son contrat, de communiquer au testeur (et futurs clients de la méthode/classe) les pré et post-conditions et invariants de classe, en Java cela se concrétise par la Javadoc (ou code pénal de l’application :) ), dans ce cadre ce n’est pas le *comment* qui est testé mais le *quoi*, autrement dit on se moque de savoir si un entier passé en paramètre de la méthode testée va faire l’objet de tests conditionnels dans une succession de blocs if-elseif-else ou d’un bloc switch-case.

    Je vous cite : « Les tests unitaires testent les méthodes, mais on ne teste pas individuellement chacune des lignes de code ! ». Je dirais justement que cela dépend de l’approche adoptée, en reprenant mon exemple précédent sur le test de l’entier, dans une approche « black-box testing » c’est tout à fait vrai, je dirais même qu’on ne teste aucune ligne de code, par contre dans une approche « white-box testing » toutes les lignes de code devraient être testées (nous avons un /extrême/ de cette approche dans le TDD), en effet si le testeur se concentre sur l’implémentation il ne rédigera pas les mêmes tests selon l’implémentation en blocs if-elseif-else ou en bloc switch-case et, dans ce dernier cas, le comportement particulier que peut avoir la méthode en raison du « fall-through ».

    Pour moi les « black- » et « white-box testing » sont une question d’approches voire de méthodes pouvant être appliquées à différents niveaux d’abstraction et non chacune réservée à un niveau qui lui serait propre (« white-box testing » = tests unitaires et « black-box testing » = tests fonctionnels, pour faire simple).

    La catégorisation des tests n’est pas chose aisée, nous pourrions combiner les deux approches avec les notions de « test-first » et « test-after » mais ce serait s’éloigner encore un peu plus du sujet.

    À vous lire,
    Frank.

  6. Publié par , Il y a 11 années

    Bonjour Frank,

    Je crois que notre définition converge maintenant. Votre phrase « Pour moi les “black-” et “white-box testing” sont une question d’approches voire de méthodes pouvant être appliquées à différents niveaux d’abstraction et non chacune réservée à un niveau qui lui serait propre » résume à peu près l’idée, même si je reste persuadé que dans la pratique et en général on est quand même très souvent dans le schéma black-boxing pour les tests fonctionnels et white-boxing pour les tests unitaires.

    Je n’ai pas encore eu l’occasion de mettre en pratique le TDD malheureusement, je n’ai pas le recul nécessaire peut être, donc votre retour d’expérience m’interesse notamment au niveau des coûts (temps, organisation, formation des developpeurs, …).

    Peut être pourrions-nous continuer cette discussion ailleurs que sur le blog. Vous pouvez me contacter sur cet email : agarciaxebiafr

    Merci et à bientôt.

  7. Publié par , Il y a 11 années

    Bonjour,

    Tout d’abord, merci pour votre article qui se révèle très instructif.

    Dans le cadre de mon projet, nous avons un serveur d’intégration continue (hudson) , qui couplé avec Sonar, nous permet d’obtenir les informations relatives au code coverage.
    Au niveau du poste de dévelopement, nous utilisons le plugin Eclipse Eclemma.

    Notre problème est que l’on constate de grosse différences au niveau des métriques qui sont remontées par ces 2 outils.
    Dans le pire des cas que j’ai pu observé, Sonar remonte un code coverage de 36.5% pour une certaine classe, alors que pour la même classe, Eclemma calcule une valeur de 79.6%.
    D’une facon générale, les métriques remontées par Sonar sont toujours plus faible (de 5 à 40%) que celles remontées par Eclemma.

    Cela vient surement de la méthode de calcul qui diffère entre Cobertura et Emma.

    Quel est votre avis sur le sujet?
    Quelle méthode de calcul est la plus pertinente?

    Merci d’avance,

    Sébastien

  8. Publié par , Il y a 9 années

    Bonjour je voudrais installer corbertura dans mon usine logicielle mais je ne sais pas comment faire? J’utilise Cruise control, maven2, subversion et ant.
    SVP aidez moi ou si klk1 a des tutos je lui serai vraiment reconnaissant.
    Merci

  9. Publié par , Il y a 8 années

    quelle sont les fonctions entrées et sorties en java ?

Laisser un commentaire

Votre adresse de messagerie ne sera pas publiée. Les champs obligatoires sont indiqués avec *

Nous recrutons

Être un Xebian, c'est faire partie d'un groupe de passionnés ; C'est l'opportunité de travailler et de partager avec des pairs parmi les plus talentueux.