Couverture de code Python

Coverage.py

deusyss

Lors de l'écriture d'un code, il peut être intéressant de contrôler la qualité de celui-ci. L'article qui suit propose de découvrir un outil de référence permettant ceci : coverage.py

Commentez

- Titre : Couverture de code Python

- Auteur : deusyss

- Parution : 5 mars 2015

- Mise à jour : 5 mars 2015

- Public visé :

- Licence : Le contenu de cet article est rédigé par deusyss et est mis à disposition selon les termes de la Licence Creative Commons Attribution - Pas d'Utilisation Commerciale 3.0 non transposé. Les logos Developpez.com, en-tête, pied de page, css, et look & feel de l'article sont Copyright ® 2013 Developpez.com.

Sommaire

- I. Introduction

- II. Principe de fonctionnement

- III. Installation

- IV. Utilisation

- IV-A. Cas de test

- IV-B. Lancement

- IV-B-1. Exclure certains modules de l'analyse

- IV-B-2. Purger les anciennes données

- IV-C. Rapport basique de test

- IV-C-1. Exclusion de modules lors de la génération du rapport

- IV-C-2. Annoter directement le code source

- IV-D. Rapport XML

- IV-E. Rapport HTML basique avec coverage

- V. Interprétation des résultats

- VI. Allons plus loin

- VII. Conclusion

- VIII. Remerciements

I - Introduction

Cet outil, créé par Ned Batchelder en 2009, permet de mesurer la quantité de code couvert lors de l'exécution de tests. Le but est d'obtenir des informations afin de connaître les sections de code non testées et de créer ainsi les tests s'y rapportant.

Pour cela, l'outil est capable de générer différents types de rapports en sortie, lesquels aideront le développeur dans sa tâche de consolidation des tests dits de non-régression.

Ces derniers permettent de s'assurer, lors de la modification de code existant, qu'on ne modifie pas le comportement initial.

Coverage est compatible avec les branches 2 et 3 de Python.

Coverage est un outil qui ne permet que de surveiller quelles parties de code sont testées. Il ne sert nullement à vérifier le respect des bonnes règles de codage. Pour cela, je vous renvoie vers PEP8.py et Pylint.

II - Principe de fonctionnement

Le principe de fonctionnement est très simple. Nous appelons coverage, en lui passant la commande que nous lançons habituellement pour nos tests. Il va alors regarder les différents modules qui sont accédés, et va comparer la quantité de cas possibles avec les cas testés.

L'ensemble de ces données sera rangé dans un fichier de sortie qui pourra par la suite être exploité. Par exemple, il sera ainsi possible d'afficher pour chaque module le nombre de lignes et le nombre de cas testés.

Mieux ! Il sera possible de générer un rapport HTML. Un simple clic sur le nom du module ouvre alors le code complet, et une coloration nous permet alors d'identifier aisément les lignes non testées. Il est ainsi facile de créer des jeux de tests complets pour notre code.

III - Installation

Afin d'installer cet outil, il est conseillé d'utiliser pip ou easy_install :

pip install coverage
easy_install coverage

IV - Utilisation

IV-A - Cas de test

Afin d'étayer un peu les explications théoriques, nous allons nous appuyer sur un cas pratique. Pour cela, je vous propose le code suivant pour le fichier puissance.py :

Puissance.py

# -*- Coding:utf-8 -*-
"""
Module de demo
"""

def puissance2(nb):
"""
Calcule la puissance 2 d'un nombre
"""
if nb == 0:
print("Ce cas ne sera pas analyse")
return nb ** 2

def puissance3(nb):
"""
Calcule la puissance 3 d'un nombre
"""
return nb ** 3

def puissance4(nb):
"""
Calcule la puissance 4 d'un nombre
"""
return nb ** 4

Ensuite le fichier de test unitaire, TU.py, pour ce module :

TU.py

# -*- Coding:utf-8 -*-
import unittest
import puissance

class TestPuissance(unittest.TestCase):
def test_puissance2(self):
retour = puissance.puissance2(2)
self.assertEqual(retour, 4)

def test_puissance3(self):
retour = puissance.puissance3(2)
self.assertEqual(retour, 8)

def test_puissance4(self):
retour = puissance.puissance4(2)
self.assertEqual(retour, 10)  # ce cas tombera en echec

if __name__ == "__main__":
unittest.main()

Pour rappel, unittest est le module fourni avec Python et permettant de réaliser des tests unitaires. Pour plus d'informations, reportez-vous à cette page.

Si nous lançons TU.py, via la commande « python TU.py », nous obtenons le résultat suivant :

Cela correspond bien à nos attentes. Cependant, aucune vérification sur le nombre suffisant de tests n'a été mise en place. Nous allons donc étudier à présent comment faire ceci.

IV-B - Lancement

L'utilisation de coverage est très simple. Il suffit d'utiliser la commande coverage run suivie de la ligne qu'on lancerait normalement.

À partir de maintenant, les commandes devront être lancées depuis le dossier contenant votre code.

coverage run TU.py

Notons que la commande python est implicite ici. Avant vous lanciez python mon_script_de_test.py, vous lancerez désormais coverage run mon_script_de_test.py.

Une fois cette commande lancée, un fichier .coverage est créé. Celui-ci contient l'ensemble des éléments nécessaires pour l'exploitation future.

IV-B-1 - Exclure certains modules de l'analyse

Analyser le code est une bonne chose, mais il peut être parfois nécessaire d'exclure certains modules. Dans notre exemple TU n'est pas à analyser, car il contient uniquement des tests unitaires. Voici la méthode pour faire cette exclusion.

coverage run --omit TU.py TU.py
coverage report

Si on désire omettre plusieurs modules, il faut utiliser le caractère de séparation « , » et il ne doit surtout pas y avoir d'espace après les virgules.

Ex : --omit

Cette option fonctionne également avec la génération de rapport que nous allons voir après :

coverage run TU.py
coverage report --omit TU.py

On constatera que seuls les résultats attendus sont affichés.

IV-B-2 - Purger les anciennes données

Avant de lancer toute nouvelle mesure, il est recommandé de purger les mesures précédentes, afin d'éviter toute erreur :

coverage erase

IV-C - Rapport basique de test

Par défaut, coverage génère un rapport basique au format texte en ligne de commande :

L'option « -m » vous permettra d'avoir en plus les numéros de lignes.

coverage report

On peut voir ici que 89 % du code de puissance.py est couvert. On a donc oublié un cas de test.

coverage report -m

Avec l'option « -m », on peut identifier la/les ligne(s) posant problème, ici la ligne 11. Cette ligne correspond à notre if.

IV-C-1 - Exclusion de modules lors de la génération du rapport

La procédure vue pour le lancement fonctionne aussi pour la génération du rapport de tests.

coverage run TU.py
coverage report --omit TU.py

IV-C-2 - Annoter directement le code source

Coverage permet aussi de lire directement le résultat dans le code :

coverage run TU.py
coverage annotate -d ./Annotate

En complément, l'option « -d » permet de demander une copie dans un dossier à spécifier. Ce sont ces copies qui seront annotées et non les originaux. Cela est très pratique pour faire un retour au développeur.

Trois caractères possibles sont alors disposés en tout début de ligne (position 0) :

Symbole

Description

>

Ligne exécutée

!

Ligne non exécutée

- ou rien

Ligne ignorée (docstring, commentaires……)

# -*- Coding:utf-8 -*-
> """
> Module de demo
> """

> def puissance2(nb):
>     """
>     Calcule la puissance 2 d'un nombre
>     """
>     if nb == 0:
!         print("Ce cas ne sera pas analyse")
>     return nb ** 2

> def puissance3(nb):
>     """
>     Calcule la puissance 3 d'un nombre
>     """
>     return nb ** 3

> def puissance4(nb):
>     """
>     Calcule la puissance 4 d'un nombre
>     """
>     return nb ** 4

IV-D - Rapport XML

Il peut aussi être intéressant de générer le rapport au format XML. Coverage permet de faire ceci simplement avec la commande ci-dessous :

coverage xml

<?xml version="1.0" ?>
<!DOCTYPE coverage
SYSTEM 'http://cobertura.sourceforge.net/xml/coverage-03.dtd'>
<coverage branch-rate="0" line-rate="0.8889" timestamp="1411860249373" version="3.7.1">
<!-- Generated by coverage.py: http://nedbatchelder.com/code/coverage -->
<packages>
<package branch-rate="0" complexity="0" line-rate="0.8889" name="">
<classes>
<class branch-rate="0" complexity="0" filename="puissance.py" line-rate="0.8889" name="puissance">
<methods/>
<lines>
<line hits="1" number="2"/>
<line hits="1" number="6"/>
<line hits="1" number="10"/>
<line hits="0" number="11"/>
<line hits="1" number="12"/>
<line hits="1" number="14"/>
<line hits="1" number="18"/>
<line hits="1" number="20"/>
<line hits="1" number="24"/>
</lines>
</class>
</classes>
</package>
</packages>
</coverage>

Ce rapport XML, associé au fichier source, vous permettra d'appréhender aisément la structure type des rapports.

IV-E - Rapport HTML basique avec coverage

La création d'un rapport au format HTML est une des fonctionnalités les plus intéressantes de coverage.

La page d'index comprendra la liste des modules analysés, et en cliquant sur un nom, le code apparaîtra avec une coloration vous permettant d'identifier aisément les cas non traités.

coverage html
coverage html -d ./dossier_sortie

L'option « -d » permet de spécifier un dossier de sortie. Par défaut le dossier sera « ./htmlcov ».

Nous voyons sur cette image qu'une ligne est surlignée en rouge. Il s'agit du cas non traité de notre module.

V - Interprétation des résultats

Quel que soit le type de rapport choisi, vous disposerez systématiquement de toutes les informations utiles.

De plus, si vous choisissez les rapports HTML, vous aurez la possibilité d'afficher pour chaque module, le code avec un surlignage vous permettant d'identifier rapidement les manques.

Voici la signification des différentes colonnes fournies :

Désignation

Description

Statements, Stmts

Nombre de cas à tester

Missing, Miss

Nombre de cas non traités

Excluded

Nombre de cas non pris en compte (commentaires, docstrings…)

Coverage, Cover

Taux de couverture du code

VI - Allons plus loin

Les tests que nous avons effectués jusqu'ici, ayant pour but de découvrir coverage, constituent ce qu'on appelle des tests par instructions.

Il faut entendre par là que le pourcentage de couverture est calculé en prenant en compte le nombre de lignes exécutées par rapport au nombre total de lignes.

Dans notre exemple précédent, sur les neuf lignes présentes, une seule n'est pas exécutée. Cela signifie donc que 89 % des lignes de notre code est exécuté.

Cependant, il se peut que l'on désire que l'unité de nos analyses ne soit pas la ligne mais la branche. Nous analysons alors non plus notre code ligne par ligne, mais par ensemble de lignes. Nous allons ainsi tester chaque combinaison que le code peut générer. Par exemple un if peut générer deux combinaisons, selon que son prédicat soit vrai ou non.

Pour demander à coverage de changer de mode, il suffit d'utiliser l'option « --branch ».

Cette option permet de mesurer non pas quelle proportion de lignes de code a été testée, mais quelle proportion des combinaisons possibles du code a été exécutée.

Comme le montrent ces captures d'écran, le taux de couverture est passé de 89 % à 82 %.

Si nos résultats sont flagrants sur du code possédant peu de lignes, il en va différemment sur du code de plusieurs milliers de lignes. En analysant votre code avec ces deux modes que nous venons de voir, vous pourrez aisément mettre en évidence les portions non testées.

VII - Conclusion

La qualité d'un code est une chose importante, car c'est elle qui peut assurer, entre autres, la fiabilité de celui-ci.

Comme nous venons de le voir, coverage.py est un outil très pratique permettant l'amélioration du code Python. Cet outil permet aux développeurs d'améliorer leur travail.

Toutefois attention, car si coverage est extrêmement pratique, il dispose de limites.

En effet, l'analyse des tests de code par ligne et par branche ne sont pas les seuls tests de couverture existants. Il existe une multitude de types de couverture (fonctions, instructions, conditions multiples…) plus ou moins adaptés à des domaines précis (aéronautique, militaire, médical…).

Aussi, si vous désirez utiliser coverage pour vos projets, assurez-vous bien de l'adéquation de l'outil avec vos contraintes.

VIII - Remerciements

Je tiens à remercier les personnes suivantes pour leur aide durant la rédaction de cet article :

- Jiyuu

- Zoom61

- f-leb

- FRANOUCH