Quattro tipi comuni di copertura del codice

Scopri cos'è la copertura del codice e scopri quattro modi comuni per misurarla.

Hai mai sentito la frase "copertura del codice"? In questo post, esamineremo che cos'è la copertura del codice nei test e quattro modi comuni per misurarla.

La copertura del codice è una metrica che misura la percentuale di codice sorgente eseguita dai test. Ti aiuta a identificare le aree in cui potrebbero mancare test adeguati.

Spesso la registrazione di queste metriche ha il seguente aspetto:

File Dichiarazioni relative alla percentuale % Branch Funzioni% % righe Linee non coperte
file.js 90% 100% 90% 80% 89.256
coffee.js 55,55% 80% 50% 62,5% 10-11, 18

Man mano che aggiungi nuove funzionalità e test, aumentare le percentuali di copertura del codice può darti maggiore certezza che la tua applicazione sia stata testata a fondo. Tuttavia, c'è ancora molto altro da scoprire.

Quattro tipi comuni di copertura del codice

Esistono quattro modi comuni per raccogliere e calcolare la copertura del codice: copertura di funzioni, righe, rami e istruzioni.

Quattro tipi di copertura del testo.

Per vedere come ogni tipo di copertura del codice calcola la propria percentuale, considera il seguente esempio di codice per il calcolo degli ingredienti del caffè:

/* 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);
}

I test che verificano la funzione calcCoffeeIngredient sono:

/* 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({});
 
});
});

Puoi eseguire il codice e i test in questa demo in tempo reale o consultare il repository.

Copertura delle funzioni

Copertura del codice: 50%

/* coffee.js */

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

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

La copertura delle funzioni è una metrica semplice. Acquisisce la percentuale di funzioni nel codice chiamate dai test.

Nell'esempio di codice sono presenti due funzioni: calcCoffeeIngredient e isValidCoffee. I test chiamano solo la funzione calcCoffeeIngredient, quindi la copertura della funzione è del 50%.

Copertura della linea

Copertura del codice: 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 copertura riga misura la percentuale di righe di codice eseguibili eseguite dalla suite di test. Se una riga di codice non viene eseguita, significa che una parte del codice non è stata testata.

L'esempio di codice contiene otto righe di codice eseguibile (evidenziate in rosso e verde), ma i test non eseguono la condizione americano (due righe) e la funzione isValidCoffee (una riga). Ciò comporta una copertura della linea del 62,5%.

Tieni presente che la copertura delle righe non prende in considerazione le dichiarazioni, come function isValidCoffee(name) e let espresso, water;, perché non sono eseguibili.

Copertura dei rami

Copertura del codice: 80%

/* coffee.js */

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

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

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

 
return {};
}

La copertura dei rami misura la percentuale di rami o punti decisionali eseguiti nel codice, ad esempio istruzioni if o loop. Determina se i test esaminano sia i rami veri che quelli falsi delle istruzioni condizionali.

L'esempio di codice contiene cinque branche:

  1. Chiamata a calcCoffeeIngredient solo con coffeeName Segno di spunta.
  2. Chiamata a calcCoffeeIngredient con coffeeName e cup Segno di spunta.
  3. Il caffè è espresso Segno di spunta.
  4. Il caffè è americano Segno X.
  5. Altro caffè Segno di spunta.

I test coprono tutti i rami tranne la condizione Coffee is Americano. La copertura dei rami è quindi dell'80%.

Copertura della dichiarazione

Copertura del codice: 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 copertura delle istruzioni misura la percentuale di istruzioni nel codice eseguite dai test. A prima vista, potresti chiederti: "non è la stessa cosa della copertura riga?" In effetti, la copertura delle istruzioni è simile alla copertura riga, ma prende in considerazione singole righe di codice che contengono più istruzioni.

Nell'esempio di codice sono presenti otto righe di codice eseguibile, ma nove istruzioni. Riesci a individuare la riga contenente due istruzioni?

Si tratta della seguente riga: espresso = 30 * cup; water = 70 * cup;

I test coprono solo cinque delle nove affermazioni, pertanto la copertura delle affermazioni è del 55,55%.

Se scrivi sempre un'istruzione per riga, la copertura delle righe sarà simile a quella delle istruzioni.

Quale tipo di copertura del codice dovresti scegliere?

La maggior parte degli strumenti di copertura del codice include questi quattro tipi di copertura del codice comuni. La scelta della metrica di copertura del codice da dare la priorità dipende da requisiti, pratiche di sviluppo e obiettivi di test specifici del progetto.

In generale, la copertura delle dichiarazioni è un buon punto di partenza perché è una metrica semplice e facile da comprendere. A differenza della copertura delle istruzioni, la copertura dei rami e la copertura delle funzioni misurano se i test chiamano una condizione (ramo) o una funzione. Pertanto, rappresentano un passaggio naturale dopo la copertura delle dichiarazioni.

Una volta raggiunta una copertura elevata delle affermazioni, puoi passare alla copertura dei rami e delle funzioni.

La copertura dei test è la stessa della copertura del codice?

No. La copertura dei test e la copertura del codice sono spesso confuse, ma sono diverse:

  • Copertura del test: metrica qualitativa che misura l'efficacia con cui la suite di test copre le funzionalità del software. Aiuta a determinare il livello di rischio.
  • Copertura del codice: una metrica quantitativa che misura la proporzione di codice eseguita durante i test. Riguarda la quantità di codice coperta dai test.

Ecco un'analogia semplificata: immagina un'applicazione web come una casa.

  • La copertura del test misura l'efficacia dei test per coprire le stanze della casa.
  • La copertura del codice misura la quantità di codice esaminata dai test.

La copertura del codice al 100% non significa assenza di bug

Sebbene sia certamente auspicabile ottenere una copertura del codice elevata durante i test, la copertura del codice al 100% non garantisce l'assenza di bug o difetti nel codice.

Un modo senza significato per ottenere il 100% di copertura del codice

Considera il seguente test:

/* 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
 
});
});

Questo test raggiunge il 100% di copertura di funzioni, righe, rami e istruzioni, ma non ha senso perché non testa effettivamente il codice. L'affermazione expect(true).toBe(true) passerà sempre, indipendentemente dal fatto che il codice funzioni correttamente.

Una metrica sbagliata è peggio di nessuna metrica

Una metrica sbagliata può darti un falso senso di sicurezza, peggio che non avere nessuna metrica. Ad esempio, se hai una suite di test che raggiunge il 100% di copertura del codice, ma i test sono tutti privi di significato, potresti avere un falso senso di sicurezza che il codice sia ben testato. Se elimini o interrompi accidentalmente una parte del codice dell'applicazione, i test passeranno comunque, anche se l'applicazione non funziona più correttamente.

Per evitare questo scenario:

  • Revisione del test. Scrivi e rivedi i test per assicurarti che siano significativi e testa il codice in una serie di scenari diversi.
  • Utilizza la copertura del codice come linea guida, non come unica misura dell'efficacia del test o della qualità del codice.

Utilizzo della copertura del codice in diversi tipi di test

Vediamo più da vicino come utilizzare la copertura del codice con i tre tipi comuni di test:

  • Test delle unità. Sono il tipo di test migliore per raccogliere la copertura del codice perché sono progettati per coprire più piccoli scenari e percorsi di test.
  • Test di integrazione. Possono aiutarti a raccogliere la copertura del codice per i test di integrazione, ma utilizzali con cautela. In questo caso, calcoli la copertura di una porzione più grande del codice sorgente e può essere difficile determinare quali test coprono effettivamente quali parti del codice. Tuttavia, il calcolo della copertura del codice dei test di integrazione può essere utile per i sistemi legacy che non dispongono di unità ben isolate.
  • Test end-to-end (E2E). La misurazione della copertura del codice per i test E2E è difficile e complessa a causa della natura intricata di questi test. Anziché utilizzare la copertura del codice, la copertura dei requisiti potrebbe essere la soluzione migliore. Questo perché l'obiettivo dei test E2E è coprire i requisiti del test, non concentrarsi sul codice sorgente.

Conclusione

La copertura del codice può essere una metrica utile per misurare l'efficacia dei test. Può aiutarti a migliorare la qualità della tua applicazione garantendo che la logica fondamentale del codice sia ben testata.

Tuttavia, ricorda che la copertura del codice è solo una metrica. Assicurati di prendere in considerazione anche altri fattori, come la qualità dei test e i requisiti dell'applicazione.

Non è necessario puntare alla copertura del codice al 100%. Devi invece utilizzare la copertura del codice insieme a un piano di test completo che includa una serie di metodi di test, tra cui test di unità, test di integrazione, test end-to-end e test manuali.

Visualizza l'esempio di codice completo e i test con una buona copertura del codice. Puoi anche eseguire il codice e i test con questa demo dal vivo.

/* 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({});
 
});
});