Couverture du code expliquée : couverture des lignes par rapport aux branches et ce que signifie réellement 100 %

  1. Accueil
  2. Blog
  3. Tests de logiciels

Tests de logiciels

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.

8 min de lecture
Couverture du code expliquée : couverture des lignes par rapport aux succursales et ce que signifie réellement 100 %

À retenir

  • 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 if dé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

Travailler avec Stack Quora

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.

Continuez à lire

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.

Intelligence artificielle

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 →
DevOps

Variables 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 logiciel

Limitation 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 tests

Se 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 logiciels

Couverture 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 SQL

DELETE 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 SQL

Vues 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 logiciel

Interface 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 logiciel

Python *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 logiciel

Fonctions 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 artificielle

Que 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 →
DevOps

Surveillance 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 →
DevOps

Volumes 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 tests

TDD 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 logiciel

Git 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 logiciel

List 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 SQL

Jointures 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 →