Quatre types courants de couverture de code

Apprenez en quoi consiste la couverture de code et découvrez quatre méthodes courantes pour la mesurer.

Avez-vous entendu l’expression « couverture du code » ? Dans cet article, nous allons découvrir ce qu'est la couverture de code dans les tests et quatre méthodes courantes de la mesurer.

Qu'est-ce que la couverture de code ?

La couverture du code est une métrique qui mesure le pourcentage de code source exécuté par vos tests. Cela vous aide à identifier les domaines pour lesquels des tests appropriés ne sont pas nécessaires.

L'enregistrement de ces métriques se présente souvent comme suit:

Fichier Déclarations (%) % Branche Fonctions% % de lignes Lignes non couvertes
file.js 90 % 100 % 90 % 80 % 89 256
coffee.js 55,55% 80 % 50 % 62,5% 10-11, 18

À mesure que vous ajoutez de nouvelles fonctionnalités et de nouveaux tests, l'augmentation des pourcentages de couverture de code vous permet de vous assurer que votre application a été testée de manière approfondie. Cependant, il y a beaucoup à découvrir.

Quatre types courants de couverture de code

Il existe quatre façons courantes de collecter et de calculer la couverture de code: la couverture des fonctions, des lignes, des branches et des instructions.

Quatre types de couverture textuelle.

Pour voir comment chaque type de couverture de code calcule son pourcentage, examinez l'exemple de code suivant pour le calcul des ingrédients du café:

/* coffee.js */

export function calcCoffeeIngredient(coffeeName, cup = 1) {
  let espresso, water;

  if (coffeeName === 'espresso') {
    espresso = 30 * cup;
    return { espresso };
  }

  if (coffeeName === 'americano') {
    espresso = 30 * cup; water = 70 * cup;
    return { espresso, water };
  }

  return {};
}

export function isValidCoffee(name) {
  return ['espresso', 'americano', 'mocha'].includes(name);
}

Les tests qui vérifient la fonction calcCoffeeIngredient sont les suivants:

/* coffee.test.js */

import { describe, expect, assert, it } from 'vitest';
import { calcCoffeeIngredient } from '../src/coffee-incomplete';

describe('Coffee', () => {
  it('should have espresso', () => {
    const result = calcCoffeeIngredient('espresso', 2);
    expect(result).to.deep.equal({ espresso: 60 });
  });

  it('should have nothing', () => {
    const result = calcCoffeeIngredient('unknown');
    expect(result).to.deep.equal({});
  });
});

Vous pouvez exécuter le code et les tests lors de cette démonstration en direct ou consulter le dépôt.

Couverture des fonctions

Couverture du code: 50%

/* coffee.js */

export function calcCoffeeIngredient(coffeeName, cup = 1) {
  // ...
}

function isValidCoffee(name) {
  // ...
}

La couverture d'une fonction est une métrique simple. Il capture le pourcentage de fonctions de votre code appelées par vos tests.

Dans l'exemple de code, il existe deux fonctions: calcCoffeeIngredient et isValidCoffee. Les tests n'appellent que la fonction calcCoffeeIngredient. La couverture de la fonction est donc de 50%.

Couverture de ligne

Couverture du code: 62,5%

/* coffee.js */

export function calcCoffeeIngredient(coffeeName, cup = 1) {
  let espresso, water;

  if (coffeeName === 'espresso') {
    espresso = 30 * cup;
    return { espresso };
  }

  if (coffeeName === 'americano') {
    espresso = 30 * cup; water = 70 * cup;
    return { espresso, water };
  }

  return {};
}

export function isValidCoffee(name) {
  return ['espresso', 'americano', 'mocha'].includes(name);
}

La couverture des lignes mesure le pourcentage de lignes de code exécutables exécutées par votre suite de tests. Si une ligne de code reste non exécutée, cela signifie qu'une partie du code n'a pas été testée.

L'exemple de code comporte huit lignes de code exécutable (en rouge et vert), mais les tests n'exécutent pas la condition americano (deux lignes) ni la fonction isValidCoffee (une ligne). Cela se traduit par une couverture de ligne de 62,5%.

Notez que la couverture de ligne ne prend pas en compte les instructions de déclaration, telles que function isValidCoffee(name) et let espresso, water;, car elles ne sont pas exécutables.

Nombre d'agences

Couverture du code: 80%

/* coffee.js */

export function calcCoffeeIngredient(coffeeName, cup = 1) {
  // ...

  if (coffeeName === 'espresso') {
    // ...
    return { espresso };
  }

  if (coffeeName === 'americano') {
    // ...
    return { espresso, water };
  }

  return {};
}
…

La couverture des branches mesure le pourcentage de branches ou de points de décision exécutés dans le code, comme les instructions if ou les boucles. Elle détermine si les tests examinent à la fois les branches "true" et "false" des instructions conditionnelles.

L'exemple de code comporte cinq branches:

  1. Appeler calcCoffeeIngredient avec seulement coffeeName Coche.
  2. Appel de calcCoffeeIngredient avec coffeeName et cup Coche.
  3. Le café est un expresso Coche.
  4. Café à l'américaine Marque X.
  5. Autre café Coche.

Les tests couvrent toutes les branches, à l'exception de la condition Coffee is Americano. La couverture des succursales est donc de 80%.

Couverture des déclarations

Couverture du code: 55,55%

/* coffee.js */

export function calcCoffeeIngredient(coffeeName, cup = 1) {
  let espresso, water;

  if (coffeeName === 'espresso') {
    espresso = 30 * cup;
    return { espresso };
  }

  if (coffeeName === 'americano') {
    espresso = 30 * cup; water = 70 * cup;
    return { espresso, water };
  }

  return {};
}

export function isValidCoffee(name) {
  return ['espresso', 'americano', 'mocha'].includes(name);
}

La couverture des instructions mesure le pourcentage d'instructions de votre code exécutées par vos tests. À première vue, vous vous demandez peut-être si ce n'est pas la même chose que la couverture réseau. En effet, la couverture des instructions est semblable à la couverture des lignes, mais elle prend en compte les lignes de code uniques contenant plusieurs instructions.

Dans l'exemple de code, il y a huit lignes de code exécutable, mais il y a neuf instructions. Pouvez-vous repérer la ligne contenant deux instructions ?

Vérifiez votre réponse

Il s'agit de la ligne suivante: espresso = 30 * cup; water = 70 * cup;

Les tests ne portent que sur cinq des neuf affirmations.Par conséquent, la couverture des déclarations est de 55,55%.

Si vous rédigez toujours une déclaration par ligne, la couverture sera semblable à celle de votre relevé.

Quel type de couverture de code devez-vous choisir ?

La plupart des outils de couverture de code incluent ces quatre types de couverture de code courantes. Le choix de la métrique de couverture de code à privilégier dépend des exigences spécifiques du projet, des pratiques de développement et des objectifs de test.

En général, la couverture des déclarations est un bon point de départ, car il s'agit d'une mesure simple et facile à comprendre. Contrairement à la couverture des instructions, la couverture des ramifications et celle des fonctions mesurent si les tests appellent une condition (branche) ou une fonction. Il s'agit donc d'une progression naturelle après la couverture de l'instruction.

Une fois que vous avez obtenu une couverture élevée pour les relevés, vous pouvez passer à la couverture des succursales et aux fonctions.

La couverture du test est-elle identique à la couverture du code ?

Non. La couverture des tests et la couverture du code sont souvent confondues, mais elles sont différentes:

  • Couverture des tests: métrique aqualitative qui mesure dans quelle mesure la suite de tests couvre les fonctionnalités du logiciel. Elle aide à déterminer le niveau de risque encouru.
  • Couverture du code: métrique quantitative qui mesure la proportion de code exécuté pendant les tests. Elle concerne la quantité de code couverte par les tests.

Voici une analogie simplifiée: imaginez une application Web comme une maison.

  • La couverture des tests permet de mesurer l'efficacité des tests dans les pièces de la maison.
  • La couverture de code mesure la surface de la maison par laquelle les tests ont été effectués.

Une couverture de code de 100% ne signifie pas qu'il n'y a pas de bug

Bien qu'il soit certainement souhaitable d'obtenir une couverture de code élevée lors des tests, une couverture de code de 100% ne garantit pas l'absence de bugs ni de failles dans votre code.

Un moyen incompréhensible d'obtenir une couverture de code de 100 %

Prenons le test suivant:

/* coffee.test.js */

// ...
describe('Warning: Do not do this', () => {
  it('is meaningless', () => { 
    calcCoffeeIngredient('espresso', 2);
    calcCoffeeIngredient('americano');
    calcCoffeeIngredient('unknown');
    isValidCoffee('mocha');
    expect(true).toBe(true); // not meaningful assertion
  });
});

Ce test permet d'atteindre une couverture de 100% des fonctions, des lignes, des branches et des instructions, mais cela n'a aucun sens, car il ne teste pas réellement le code. L'assertion expect(true).toBe(true) est toujours transmise, que le code fonctionne correctement ou non.

Une métrique incorrecte est pire qu'aucune

Une métrique incorrecte peut donner une fausse impression de sécurité, ce qui est pire que n'avoir aucune métrique. Par exemple, si vous disposez d'une suite de tests qui affiche une couverture de code de 100 %, mais que les tests sont tous dénués de sens, vous risquez d'avoir une fausse impression de sécurité en indiquant que votre code est bien testé. Si vous supprimez ou cassez accidentellement une partie du code de l'application, les tests seront quand même réussis, même si l'application ne fonctionne plus correctement.

Pour éviter ce scénario:

  • Examen du test. Rédigez et examinez des tests pour vous assurer qu'ils sont pertinents, et testez le code dans différents scénarios.
  • Utilisez la couverture de code comme référence, et non comme la seule mesure de l'efficacité des tests ou de la qualité du code.

Utiliser la couverture de code dans différents types de tests

Examinons de plus près comment vous pouvez utiliser la couverture de code avec les trois types de tests les plus courants:

  • Tests unitaires. Ils constituent le meilleur type de test pour collecter la couverture de code, car ils sont conçus pour couvrir plusieurs petits scénarios et chemins de test.
  • Tests d'intégration. Ils permettent de collecter la couverture de code pour les tests d'intégration, mais ils sont utilisés avec prudence. Dans ce cas, vous calculez la couverture d'une plus grande partie du code source, et il peut être difficile de déterminer quels tests couvrent réellement quelles parties du code. Néanmoins, le calcul de la couverture du code des tests d'intégration peut être utile pour les anciens systèmes dont les unités ne sont pas bien isolées.
  • Tests de bout en bout (E2E). Mesurer la couverture de code pour les tests E2E est difficile et difficile en raison de la nature complexe de ces tests. Au lieu d'utiliser la couverture de code, la couverture des exigences peut être la meilleure solution. En effet, l'objectif des tests de bout en bout est de couvrir les exigences de votre test, et non de se concentrer sur le code source.

Conclusion

La couverture du code peut être une métrique utile pour mesurer l'efficacité de vos tests. Il peut vous aider à améliorer la qualité de votre application en vous assurant que la logique cruciale de votre code est bien testée.

Cependant, n'oubliez pas que la couverture de code n'est qu'une métrique parmi d'autres. Veillez également à tenir compte d'autres facteurs, tels que la qualité de vos tests et les exigences de votre application.

Viser une couverture de code de 100% n'est pas l'objectif. Vous devez plutôt utiliser une couverture de code avec un plan de test complet qui intègre diverses méthodes de test, y compris les tests unitaires, les tests d'intégration, les tests de bout en bout et les tests manuels.

Consultez l'exemple de code complet et les tests avec une bonne couverture de code. Vous pouvez également exécuter le code et effectuer des tests avec cette démonstration en direct.

/* coffee.js - a complete example */

export function calcCoffeeIngredient(coffeeName, cup = 1) {
  if (!isValidCoffee(coffeeName)) return {};

  let espresso, water;

  if (coffeeName === 'espresso') {
    espresso = 30 * cup;
    return { espresso };
  }

  if (coffeeName === 'americano') {
    espresso = 30 * cup; water = 70 * cup;
    return { espresso, water };
  }

  throw new Error (`${coffeeName} not found`);
}

function isValidCoffee(name) {
  return ['espresso', 'americano', 'mocha'].includes(name);
}
/* coffee.test.js - a complete test suite */

import { describe, expect, it } from 'vitest';
import { calcCoffeeIngredient } from '../src/coffee-complete';

describe('Coffee', () => {
  it('should have espresso', () => {
    const result = calcCoffeeIngredient('espresso', 2);
    expect(result).to.deep.equal({ espresso: 60 });
  });

  it('should have americano', () => {
    const result = calcCoffeeIngredient('americano');
    expect(result.espresso).to.equal(30);
    expect(result.water).to.equal(70);
  });

  it('should throw error', () => {
    const func = () => calcCoffeeIngredient('mocha');
    expect(func).toThrowError(new Error('mocha not found'));
  });

  it('should have nothing', () => {
    const result = calcCoffeeIngredient('unknown')
    expect(result).to.deep.equal({});
  });
});