Couverture du code expliquée : Couverture des lignes ou des succursales et ce que signifie réellement 100 %
La couverture vous indique le code exécuté par vos tests. Cela ne vous dit pas si les tests ont vérifié quelque chose. Nous avons mesuré un petit module de trois manières pour montrer la différence.
- La Couverture du code mesure la quantité de code exécutée pendant les tests.
- La couverture des succursales est plus stricte que la couverture des lignes et plus utile.
- Lors de notre test, une suite sans aucune assertion atteignait quand même une couverture de 100 %.
- Utilisez la couverture pour trouver du code non testé, et non comme preuve de qualité.
Une équipe rapporte fièrement une couverture de code de 95 %, puis envoie un bug dans une ligne qui était “couverte”. Ce n’est pas une contradiction. La couverture répond à une question précise : les tests ont-ils exécuté ce code ? Qu’ils aient vérifié le résultat est une tout autre question.
Nous avons utilisé Python avec cover.py 7.17 et pytest 9.2. Chaque numéro et rapport ci-dessous provient de notre propre course. Les idées s’appliquent dans n’importe quelle langue.
Qu’est-ce que la couverture du code ?
La couverture du code est le pourcentage de votre code qui s’exécute pendant l’exécution de vos tests automatisés. Un outil enregistre quelles lignes, et éventuellement quelles branches, ont été exécutées, puis signale ce qui a été manqué.
C’est plus utile à l’envers. Une faible couverture dans une zone vous indique avec certitude que les tests ne l’exercent pas. Une couverture élevée vous en dit beaucoup moins.
Types de couverture
| Tapez | Question à laquelle il répond |
|---|---|
| Fonction | Chaque fonction a-t-elle été appelée au moins une fois ? |
| Ligne (instruction) | Chaque ligne a-t-elle fonctionné au moins une fois ? |
| Branche | Est-ce que chacun if est allé dans les deux sens, vrai et faux ? |
| État | Chaque partie d’une condition combinée (a and b) a-t-elle pris les deux valeurs ? |
| MC/DC | Chaque condition a-t-elle indépendamment modifié le résultat ? Utilisé dans les logiciels critiques pour la sécurité. |
Chaque niveau est plus strict que celui situé au-dessus. La plupart des équipes suivent la couverture des lignes et des succursales. La couverture des conditions et MC/DC apparaissent dans les normes aéronautiques et automobiles, où le support en outillage est spécialisé.
Le code que nous avons mesuré
def shipping_fee(total, is_member, express=False):
if total < 0:
raise ValueError("total cannot be negative")
fee = 0 if total >= 1000 else 50
if is_member:
fee = max(fee - 25, 0)
if express:
fee += 100
return fee
Notre premier fichier de test comportait deux tests :
def test_regular_order():
assert shipping_fee(500, False) == 50
def test_member_express():
assert shipping_fee(500, True, express=True) == 125
Nous l’avons exécuté avec la mesure de branche activée :
coverage run --branch -m pytest test_shipping.py
coverage report -m
Lecture du rapport
Name Stmts Miss Branch BrPart Cover Missing
---------------------------------------------------------
shipping.py 9 1 6 1 87% 3
- Stmts 9, Miss 1 : neuf instructions exécutables, dont une n’a jamais été exécutée.
- Branche 6, BrPart 1 : trois
ifdécisions donnent six résultats possibles. Une décision n’allait que dans un sens. - Manquant 3 : la ligne 3, la
raise ValueError, n’a jamais été diffusée. Aucun test n’a donné un total négatif.
Le correctif est un test du chemin d’erreur. Nous avons également ajouté un test pour une commande importante :
def test_free_over_1000():
assert shipping_fee(1000, False) == 0
def test_negative_total():
with pytest.raises(ValueError):
shipping_fee(-1, False)
Avec les deux fichiers, le rapport indiquait 100 % et aucune ligne manquante ni branche partielle. Les chemins d’erreur sont les découvertes classiques de couverture des écarts, c’est pourquoi les tests d’entrée non valide, conçus avec des techniques telles que le partitionnement d’équivalence, s’y associent si bien.
Ce que le rapport cachait
Regardez à nouveau le premier rapport. Il disait que seule la ligne 3 manquait. Mais les deux premiers tests en ont utilisé un total de 500, de sorte que la règle de livraison gratuite pour les commandes de 1 000 ou plus n’a jamais été testée, et le rapport ne l’a pas signalé.
La raison en est que fee = 0 if total >= 1000 else 50 se trouve sur une seule ligne. cover.py mesure les branches entre les lignes, donc une expression conditionnelle à l’intérieur d’une seule ligne compte comme une instruction exécutée. Si cette règle comportait un bug, tel que > au lieu de >=, la première suite aurait quand même affiché 87 % sans aucune indication. Les tests de la limite elle-même, comme décrit dans analyse des valeurs limites, sont ce qui la détecte.
Couverture à 100% sans contrôle
Ensuite, nous avons écrit un test délibérément médiocre qui appelle tous les chemins mais n’affirme rien :
def test_runs_everything_but_checks_nothing():
shipping_fee(500, True, express=True)
shipping_fee(1000, False)
shipping_fee(500, False)
try:
shipping_fee(-1, False)
except ValueError:
pass
shipping.py 9 0 6 0 100%
Il a atteint 100 % de couverture de lignes et de succursales. La fonction pourrait renvoyer n’importe quel nombre et ce test réussirait quand même. Il s’agit de la limite fondamentale de la couverture : elle mesure l’exécution et non la vérification.
Conseil : Lorsque vous examinez une pull request, lisez les assertions, pas seulement le changement de couverture. Un test qui ajoute une couverture sans assertion significative ne fait qu’ajouter une fausse confiance.
Pour mesurer si les tests détectent réellement les bogues, utilisez les tests de mutation. Un outil tel que Mutmut pour Python, PIT pour Java ou Stryker pour JavaScript apporte de petites modifications à votre code, comme retourner >= à >, et vérifie qu’un test échoue. Notre test sans assertion permettrait à chaque mutation de survivre.
Quel objectif fixer
Il n’existe pas de chiffre universellement correct. Conseils pratiques :
- Ne recherchez pas 100 %. Les derniers pourcentages signifient souvent tester du code trivial ou écrire des tests qui touchent simplement des lignes.
- De nombreuses équipes visent environ 70 à 85 % au total, avec des attentes plus élevées en matière de logique métier de base.
- Surveillez la tendance, pas le chiffre. La couverture qui diminue à chaque version est un signe d’avertissement.
- Exiger la couverture du nouveau code dans les demandes d’extraction plutôt que de forcer l’ancien code à s’afficher d’un seul coup.
- Excluez ce qui n’a pas d’importance, comme le code généré, avec des règles claires.
La loi de Goodhart s’applique : lorsqu’une mesure devient un objectif, elle cesse d’être une bonne mesure. Les cibles difficiles encouragent les tests comme notre exemple sans assertion.
Outils par langue
| Langue | Outils communs |
|---|---|
| Python | couverture.py, pytest-cov |
| JavaScript/TypeScript | Istanbul (New York), intégré à Jest et Vitest |
| Java | JaCoCo |
| .NET | Couvre-lit |
| Allez | allez tester -couvrir |
| Rapports | Codecov, SonarQube, Combinaisons |
Couverture en CI
Exécutez la couverture dans le cadre de votre pipeline CI et publiez le rapport afin que les réviseurs puissent voir quelles nouvelles lignes n’ont pas été testées. Avec cover.py, coverage report --fail-under=80 échoue la construction en dessous d’un seuil. Gardez tout seuil modeste et combinez-le avec une règle sur le nouveau code, afin qu’un seul gros fichier hérité ne bloque pas chaque modification.
Au-delà de la couverture : de meilleures questions à poser
- Les tests couvrent-ils les parties du code à risque et fréquemment modifiées ?
- Les limites et les chemins d’erreur sont-ils testés, et pas seulement le chemin heureux ?
- Un test échouerait-il si cette ligne était fausse ?
- Les tests sont-ils suffisamment rapides et stables pour être exécutés à chaque modification ?
Ceux-ci appartiennent à n’importe quelle stratégie de test. La couverture permet de répondre uniquement à la première.
Exclusion de code et lecture de rapports HTML
Certains codes ne valent pas la peine d’être testés directement, comme les aides au débogage ou un bloc qui ne s’exécute que lorsqu’un script est démarré manuellement. cover.py vous permet d’exclure des lignes avec un commentaire :
if __name__ == "__main__": # pragma: no cover
main()
Vous pouvez également définir des exclusions et les dossiers sources à mesurer dans la configuration, par exemple dans pyproject.toml :
[tool.coverage.run]
branch = true
source = ["shop"]
[tool.coverage.report]
show_missing = true
exclude_also = ["if TYPE_CHECKING:"]
Utilisez les exclusions avec parcimonie et examinez-les comme du code. Chacun est une décision selon laquelle certaines lignes ne seront jamais vérifiées. L’exclusion d’un gestionnaire d’erreurs difficile à tester simplement pour augmenter le nombre va à l’encontre de l’objectif.
Pour le travail quotidien, le rapport HTML est plus utile que le tableau terminal. Exécutez coverage html et ouvrez htmlcov/index.html. Chaque fichier est affiché avec les lignes exécutées en vert, les lignes manquées en rouge et les branches partielles en jaune, afin que vous puissiez voir d’un coup d’œil lequel if n’a jamais pris son autre chemin. De nombreux éditeurs peuvent également afficher une couverture dans la marge, ce qui rend les espaces visibles pendant que vous écrivez le code plutôt qu’après.
Couverture avant la refactorisation
La couverture est également un contrôle de sécurité avant la refactorisation. Si vous envisagez de restructurer un module, consultez d’abord son rapport. Les zones rouges ne comportent aucun test pour vous avertir si le comportement change. Ajoutez donc des tests de caractérisation qui capturent le comportement actuel avant de commencer. Utilisée de cette manière, la couverture cesse d’être une cible et devient un outil de planification pratique.
Le même rapport est utile lorsqu’un bug échappe à la production. Recherchez la ligne où se trouve le bug et vérifiez si un test l’a exécuté. Sinon, vous avez trouvé une lacune qui mérite d’être comblée. Si un test l’a exécuté, le problème est une assertion faible, ce qui est exactement la situation montrée par notre exemple sans assertion. Les deux réponses vous indiquent comment améliorer la suite.
Pour conclure
La couverture du code est une lampe de poche, pas une note. Utilisez la couverture des branches pour trouver du code qu’aucun test ne touche, en particulier la gestion des erreurs, puis écrivez des tests qui font de véritables assertions. Si vous vous souvenez d’un résultat de notre analyse, notez celui-ci : une couverture de 100 % a été atteinte par un test qui n’a rien vérifié.
Questions fréquentes
Qu’est-ce que la couverture du code dans les tests ?
Il s’agit du pourcentage de code exécuté lors de l’exécution de tests automatisés, mesuré par lignes, branches ou conditions. Il montre quels tests ont touché, et non s’ils l’ont vérifié.
Quelle est la différence entre la couverture des lignes et celle des succursales ?
La couverture de ligne vérifie que chaque ligne est exécutée. La couverture des succursales vérifie que chaque décision va dans les deux sens. La couverture des branches trouve les chemins non testés que la couverture des lignes manque.
Une couverture de code à 100 % est-elle bonne ?
Pas tout seul. Lors de notre test, une suite sans assertion a atteint 100 %. Visez des tests significatifs sur du code important plutôt que sur un nombre parfait.
Qu’est-ce qu’un bon pourcentage de couverture de code ?
De nombreuses équipes ciblent globalement environ 70 à 85 %, avec davantage de logique critique. La tendance et la qualité des affirmations comptent plus que leur nombre exact.
En quoi les tests de mutation sont-ils différents de la couverture ?
Les tests de mutation modifient légèrement votre code et vérifient qu’un test échoue. Il mesure si les tests détectent des bugs, ce que la couverture ne peut pas faire.
Continuez à apprendre
Besoin d’aide pour les tests, WordPress ou le référencement ?
Nous aidons les équipes à proposer de meilleurs logiciels et à développer leurs sites. Dites-nous sur quoi vous travaillez et nous vous proposerons une prochaine étape claire.
Plus de guides sur tests, IA et développement moderne
Vous avez apprécié cet article ? Choisissez un sujet et accédez directement à nos nouveaux didacticiels.
Les hallucinations de l'IA expliquées : pourquoi les modèles de langage inventent les choses et comment les réduire
Les hallucinations de l'IA ont expliqué : ce qu'elles sont, pourquoi les modèles de langage les produisent, les types courants, comment les réduire …
Lire l'article →DevOpsVariables d'environnement expliquées : fichiers .env, secrets et configuration correctement effectués
Variables d'environnement expliquées avec des exemples Python que nous avons exécutés : lecture et définition, pourquoi chaque valeur est une chaîne…
Lire l'article →Développement logicielLimitation du débit de l'API expliquée : algorithmes, en-têtes et gestion des erreurs 429
Limitation du débit de l'API expliquée avec une simulation que nous avons exécutée : compartiment de jetons, fenêtre fixe, fenêtre glissante, 429 ré…
Lire l'article →Automatisation des testsSe moquer en Python : unittest.mock, patch et les erreurs qui font mentir les tests
Moquerie en Python avec unittest.mock, testée : Mock et return_value, side_effect, correctifs où un nom est utilisé, spec et autospec, dates moqueus…
Lire l'article →Tests logicielsCouverture du code expliquée : couverture des lignes par rapport aux branches et ce que signifie réellement 100 %
Couverture du code expliquée avec une véritable exécution de cover.py : couverture des lignes, des branches et des conditions, lecture d'un rapport,…
Lire l'article →Bases de données et SQLDELETE vs TRUNCATE vs DROP dans SQL : différences, restauration et quand les utiliser
DELETE vs TRUNCATE vs DROP expliqué avec SQL testé : ce que chacun supprime, restauration, déclencheurs, réinitialisation d'identité, vitesse et en …
Lire l'article →Bases de données et SQLVues SQL expliquées : créez, utilisez et évitez les pièges, avec des exemples
Vues SQL expliquées avec les requêtes que nous avons exécutées : création d'une vue, résultats en direct, masquage des colonnes, performances, vues …
Lire l'article →Développement logicielInterface TypeScript vs Type : différences réelles, testées avec le compilateur
Interface TypeScript vs type, vérifiée avec le vrai compilateur : ce que les deux peuvent faire, ce qu'un seul peut faire, la fusion des déclaration…
Lire l'article →Développement logicielPython *args et **kwargs expliqués : fonctions flexibles avec des exemples clairs
Python *args et **kwargs expliqués avec le code que nous avons exécuté : emballage et déballage, ordre des paramètres, arguments de mot-clé uniqueme…
Lire l'article →Développement logicielFonctions Python Lambda : syntaxe, utilisations réelles et quand ne pas les utiliser
Fonctions Python lambda expliquées avec le code que nous avons exécuté : syntaxe, tri avec clé, carte et filtre, fermetures, piège de liaison tardiv…
Lire l'article →Intelligence artificielleQue sont les agents IA ? Comment ils fonctionnent, avec une simple boucle d'agent que vous pouvez exécuter
Que sont les agents IA ? Un guide simple sur la boucle d'agent, les outils, la mémoire et les garde-fous, avec une petite boucle Python que nous avo…
Lire l'article →DevOpsSurveillance vs observabilité : la vraie différence, avec les journaux, les métriques et les traces
Surveillance et observabilité expliquées : échecs connus et inconnus, journaux, métriques et traces, présentation pas à pas des incidents de paiemen…
Lire l'article →DevOpsVolumes Docker vs montages Bind : conserver les données du conteneur de la bonne manière
Volumes Docker et montages de liaison expliqués : pourquoi les données du conteneur disparaissent, volumes nommés, montages de liaison, tmpfs, exemp…
Lire l'article →Automatisation des testsTDD vs BDD : principales différences, une fonctionnalité conçue dans les deux sens et quand les utiliser
TDD vs BDD expliqué avec une fonctionnalité de remise construite dans les deux sens : refactor rouge-vert avec le code que nous avons exécuté, scéna…
Lire l'article →Développement logicielGit merge vs rebase : différences, exemples et quand utiliser chacun
Git merge vs rebase expliqué avec de vraies sorties de log, un tableau comparatif, la règle d'or du rebase et un flux de travail sûr pour les équipes.
Lire l'article →Développement logicielList comprehension Python : syntaxe, exemples et cas à éviter
La list comprehension Python expliquée : syntaxe, filtres, if-else, boucles imbriquées, temps mesurés et cas où une boucle for classique est préféra…
Lire l'article →Bases de données et SQLJointures SQL expliquées avec exemples (Inner, Left, Right, Full)
Comprenez les jointures SQL pas à pas avec des tables et requêtes d'exemple : INNER, LEFT, RIGHT, FULL, CROSS et SELF, plus erreurs courantes et con…
Lire l'article →