Qu’est-ce que la couverture de code ?
La couverture de code est la part des lignes, ou des branches, d’un programme que les tests ont exécutées. Elle montre le code qu’aucun test n’a jamais lancé, mais elle ne dit pas si les tests vérifient quelque chose.
Une équipe annonce « 100 % de couverture » et se croit à l’abri. Cette leçon montre pourquoi ce chiffre rassure plus qu’il ne prouve.
À la fin de cette leçon
Remarque
Je peux lire un taux de couverture et dire ce qu’il ne montre pas.
Mots-clés
| Mot | Ce que ça veut dire | Exemple |
|---|---|---|
| Couverture de lignes | la part des lignes exécutées au moins une fois par les tests | |
| Branche | un des deux chemins d’un if : la condition vraie, ou fausse | |
| Couverture de branches | la part des branches prises au moins une fois par les tests | |
| Trace | la liste des lignes qu’un test a exécutées, dans l’ordre | |
| Taux de couverture | le pourcentage de lignes ou de branches exécutées | 5 lignes sur 6, soit 83 % |
| Ligne non couverte | une ligne qu’aucun test n’a exécutée |
Les cellules que les étudiants remplissent restent vides dans cet aperçu.
La couverture de lignes compte les lignes exécutées
Un outil de couverture suit les tests et note chaque ligne qu’ils exécutent. À la fin, il divise le nombre de lignes exécutées par le nombre de lignes exécutables.
Prenons une petite fonction qui applique une remise aux clients qui ont une carte de fidélité. Le tableau suit deux tests.
Une fonction de quatre lignes
def prix_final(prix, carte):
if carte:
prix = prix - prix // 10
return prix
# Test A : avec carte. Test B : sans carte.
assert prix_final(1000, True) == 900
assert prix_final(1000, False) == 1000
print("Les deux chemins sont essayés")
Exemple travaillé : les lignes exécutées
| Ligne | Code | Test A : carte vraie | Test B : carte fausse |
|---|---|---|---|
| 1 | def prix_final(prix, carte): | oui | oui |
| 2 | if carte: | oui | oui |
| 3 | prix = prix - prix // 10 | oui | non |
| 4 | return prix | oui | oui |
Les cellules que les étudiants remplissent restent vides dans cet aperçu.
Calculer un taux de couverture
Le test A seul exécute les lignes 1, 2, 3 et 4 : quatre lignes sur quatre, soit 100 %. Le test B seul n’exécute pas la ligne 3 : trois lignes sur quatre, soit 75 %.
La formule est simple : lignes exécutées × 100, divisé par lignes exécutables. Avec 5 lignes sur 6, on obtient 83 %.
Les branches montrent ce que les lignes cachent
Le test A seul exécute toutes les lignes, mais il ne fait jamais passer carte à faux. Le chemin « sans carte » n’est jamais pris.
Un if a deux branches : la condition vraie et la condition fausse. Le test A prend une branche sur deux : la couverture de branches est de 50 %, alors que celle des lignes est de 100 %.
Exemple travaillé : lignes contre branches
| Tests lancés | Lignes couvertes | Branches couvertes |
|---|---|---|
| A seul (carte vraie) | 4 sur 4, soit 100 % | 1 sur 2, soit 50 % |
| B seul (carte fausse) | 3 sur 4, soit 75 % | 1 sur 2, soit 50 % |
| A et B | 4 sur 4, soit 100 % | 2 sur 2, soit 100 % |
Les cellules que les étudiants remplissent restent vides dans cet aperçu.
Écrire la fonction de fidélité
printf 'def prix_final(prix, carte):\n if carte:\n prix = prix - prix // 10\n return prix\n' > essais/couv.pyAffichée ici, non exécutée. Créez un compte gratuit pour appuyer sur Exécuter et la saisir dans le terminal de votre propre labo.
Écrire le test A seul
printf 'import unittest\nfrom couv import prix_final\n\n\nclass TestsCouv(unittest.TestCase):\n def test_avec_carte(self):\n self.assertEqual(prix_final(1000, True), 900)\n' > essais/test_couv.pyAffichée ici, non exécutée. Créez un compte gratuit pour appuyer sur Exécuter et la saisir dans le terminal de votre propre labo.
Lancer le test A : vert, 100 % des lignes
python3 -m unittest discover -s essais -p "test_couv.py" 2>&1 | tail -1Affichée ici, non exécutée. Créez un compte gratuit pour appuyer sur Exécuter et la saisir dans le terminal de votre propre labo.
Une branche reste sans test
Le résultat est OK, et toutes les lignes sont exécutées. Pourtant la fonction n’a jamais été essayée sans carte.
Si quelqu’un casse ce chemin demain, rien ne sera rouge. La commande qui suit essaie ce chemin à la main, comme le ferait le test B.
Essayer le chemin sans carte
PYTHONPATH=essais python3 -c "from couv import prix_final; print(prix_final(1000, False))"Affichée ici, non exécutée. Créez un compte gratuit pour appuyer sur Exécuter et la saisir dans le terminal de votre propre labo.
Un test à 100 % peut ne rien vérifier
La couverture compte les lignes exécutées, pas les lignes vérifiées. Un test qui appelle la fonction sans rien comparer couvre tout, et ne prouve rien.
Écrivons une fonction fausse, qui rend toujours 0, et un test qui l’appelle sans assertion.
Écrire une fonction fausse
printf 'def prix_final(prix, carte):\n if carte:\n prix = 0\n return 0\n' > essais/couv_faux.pyAffichée ici, non exécutée. Créez un compte gratuit pour appuyer sur Exécuter et la saisir dans le terminal de votre propre labo.
Écrire un test sans assertion
printf 'import unittest\nfrom couv_faux import prix_final\n\n\nclass TestsVide(unittest.TestCase):\n def test_appel_sans_verifier(self):\n prix_final(1000, True)\n' > essais/test_couv_faux.pyAffichée ici, non exécutée. Créez un compte gratuit pour appuyer sur Exécuter et la saisir dans le terminal de votre propre labo.
Le test est vert, la fonction est fausse
python3 -m unittest discover -s essais -p "test_couv_faux.py" 2>&1 | tail -1Affichée ici, non exécutée. Créez un compte gratuit pour appuyer sur Exécuter et la saisir dans le terminal de votre propre labo.
Erreur fréquente
Attention
Beaucoup pensent que 100 % de couverture veut dire un programme sans faute. Ce chiffre dit seulement que chaque ligne a tourné une fois. Il ne dit rien sur les assertions, ni sur les valeurs essayées.
Calculer la couverture à partir de traces
Dans le devoir, une trace est la liste des numéros de lignes qu’un test a exécutées. Vous recevez les lignes exécutables et une liste de traces, une par test.
Une ligne est couverte si au moins une trace la contient. couverture(total, traces) rend le pourcentage entier, arrondi vers le bas. lignes_manquantes(total, traces) rend les lignes jamais exécutées, dans l’ordre.