Quatre types courants de couverture de code

Découvrez en quoi consiste la couverture du code et quatre méthodes courantes pour la mesurer.

Avez-vous déjà entendu l'expression "couverture du code" ? Dans cet article, nous allons découvrir en quoi consiste la couverture du code dans les tests et quatre méthodes courantes pour la mesurer.

La couverture de code est une métrique qui mesure le pourcentage de code source exécuté par vos tests. Cela vous aide à identifier les domaines qui ne sont peut-être pas suffisamment testés.

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

Fichier Instructions% % de la branche Fonctions% % 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, augmenter le pourcentage de couverture de code peut vous donner plus de confiance dans le fait que votre application a été testée de manière approfondie. Mais il y a d'autres choses à découvrir.

Quatre types courants de couverture du code

Il existe quatre façons courantes de collecter et de calculer la couverture du code: la couverture de la fonction, de la ligne, de la branche et de l'instruction.

Quatre types de couverture textuelle.

Pour voir comment chaque type de couverture de code calcule son pourcentage, consultez l'exemple de code suivant pour calculer les 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 dans 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 fonctionnelle est une métrique simple. Il capture le pourcentage de fonctions de votre code que vos tests appellent.

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 la 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 en vert), mais les tests n'exécutent pas la condition americano (deux lignes) ni la fonction isValidCoffee (une ligne). La couverture de la ligne est donc de 62,5%.

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

Couverture des branches

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, tels que les instructions "si" ou les boucles. Il détermine si les tests examinent à la fois les branches "true" et "false" des instructions conditionnelles.

L'exemple de code comporte cinq branches:

  1. Appel de calcCoffeeIngredient avec uniquement coffeeName Coche.
  2. Appeler calcCoffeeIngredient avec coffeeName et cup Coche.
  3. Le café est un expresso Coche.
  4. Le café est un Americano X
  5. Autre café Coche.

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

Couverture des énoncés

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. Au premier abord, vous pourriez vous demander "n'est-ce pas la même chose que la couverture de ligne ?" 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 neuf instructions. Pouvez-vous repérer la ligne contenant deux instructions ?

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

Les tests ne couvrent que cinq des neuf instructions.La couverture des instructions est donc de 55, 55%.

Si vous écrivez toujours une instruction par ligne, votre couverture des lignes sera semblable à celle des instructions.

Quel type de couverture de code choisir ?

La plupart des outils de couverture de code incluent ces quatre types de couverture de code courants. Le choix de la métrique de couverture du code à prioriser 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 instructions est un bon point de départ, car il s'agit d'une métrique simple et facile à comprendre. Contrairement à la couverture des instructions, la couverture des branches et la couverture des fonctions mesurent si les tests appellent une condition (branche) ou une fonction. Par conséquent, elles constituent une progression naturelle après la couverture des instructions.

Une fois que vous avez atteint une couverture élevée des instructions, vous pouvez passer à la couverture des branches et des fonctions.

La couverture des tests est-elle la même que 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 qualitative qui mesure dans quelle mesure la suite de tests couvre les fonctionnalités du logiciel. Il permet de déterminer le niveau de risque encouru.
  • Couverture du code: métrique quantitative qui mesure la proportion de code exécuté pendant les tests. Il s'agit de la quantité de code couverte par les tests.

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

  • La couverture des tests mesure dans quelle mesure les tests couvrent les pièces de la maison.
  • La couverture du code mesure la partie de la maison que les tests ont explorée.

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

Bien qu'il soit 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 ou de défauts dans votre code.

Méthode inutile pour obtenir une couverture de code à 100 %

Prenons l'exemple 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 offre une couverture de 100% pour les fonctions, les lignes, les branches et les instructions, mais cela n'a aucun sens, car il ne teste pas réellement le code. L'assertion expect(true).toBe(true) réussit toujours, que le code fonctionne correctement ou non.

Une mauvaise métrique est pire qu'aucune métrique

Une mauvaise métrique peut vous donner un faux sentiment de sécurité, ce qui est pire que de ne pas avoir de métrique du tout. Par exemple, si vous disposez d'une suite de tests qui atteint une couverture de code de 100 %, mais que les tests sont tous dépourvus de sens, vous pouvez avoir un faux sentiment de sécurité quant à la qualité de test de votre code. Si vous supprimez ou endommagez accidentellement une partie du code de l'application, les tests seront toujours concluants, même si l'application ne fonctionne plus correctement.

Pour éviter ce scénario:

  • Examen des tests Écrivez 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 guide, et non comme seule mesure de l'efficacité des tests ou de la qualité du code.

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

Voyons comment utiliser la couverture du code avec les trois types de tests courants:

  • Tests unitaires. Il s'agit du meilleur type de test pour collecter la couverture du code, car ils sont conçus pour couvrir plusieurs petits scénarios et chemins de test.
  • Tests d'intégration. Ils peuvent aider à collecter la couverture du code pour les tests d'intégration, mais utilisez-les avec précaution. 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. Toutefois, calculer la couverture du code des tests d'intégration peut être utile pour les anciens systèmes qui ne disposent pas d'unités bien isolées.
  • Tests de bout en bout (E2E, end-to-end). Mesurer la couverture du code pour les tests E2E est difficile et complexe 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 E2E 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 essentielle de votre code est bien testée.

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

L'objectif n'est pas d'atteindre une couverture de code de 100 %. À la place, vous devez utiliser la couverture de code avec un plan de test complet qui intègre diverses méthodes de test, y compris des tests unitaires, des tests d'intégration, des tests de bout en bout et des tests manuels.

Consultez l'exemple de code complet et les tests avec une bonne couverture du code. Vous pouvez également exécuter le code et les tests avec cette démo 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({});
  });
});