Quattro tipi comuni di copertura del codice

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

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

Che cos'è la copertura del codice?

La copertura del codice è una metrica che misura la percentuale di codice sorgente eseguiti dai test. Ti aiuta a identificare le aree che potrebbero non avere test adeguati.

Spesso, la registrazione di queste metriche ha il seguente aspetto:

File % estratti conto % ramo % funzioni % righe Righe scoperte
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, l'aumento delle percentuali di copertura del codice può darti una maggiore sicurezza del fatto che la tua applicazione sia stata testata accuratamente. Ma c'è ancora molto da scoprire.

Quattro tipi comuni di copertura del codice

Esistono quattro modi comuni per raccogliere e calcolare la copertura del codice: funzione, riga, ramo e copertura istruzioni.

Quattro tipi di copertura del testo.

Per vedere come viene calcolata la percentuale di ogni tipo di copertura di codice, 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 su questa demo dal vivo o dare un'occhiata al repository.

Copertura della funzione

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 è al 50%.

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

L'esempio di codice ha 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ò si traduce in una copertura di linea del 62,5%.

Tieni presente che la copertura a righe non prende in considerazione le dichiarazioni relative alla dichiarazione, 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 o loop if. Determina se i test esaminano sia i rami vero che falso delle istruzioni condizionali.

Nell'esempio di codice sono presenti cinque rami:

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

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

Copertura dell'estratto conto

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

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

Verificare la risposta

È la seguente riga: espresso = 30 * cup; water = 70 * cup;

I test riguardano solo cinque delle nove dichiarazioni, pertanto la copertura delle dichiarazioni è del 55,55%.

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

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 comune. La scelta della metrica di copertura del codice a cui dare la priorità dipende da requisiti di progetto, pratiche di sviluppo e obiettivi di test specifici.

In generale, la copertura delle dichiarazioni è un buon punto di partenza, in quanto si tratta di una metrica semplice e di facile comprensione. 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'evoluzione naturale dopo la copertura dell'affermazione.

Una volta raggiunta una copertura elevata delle istruzioni, è possibile passare alla copertura dei rami e delle funzioni.

La copertura del test equivale alla copertura del codice?

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

  • Copertura dei test: metrica acqualitativa che misura in che misura 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 eseguito durante il test. ma riguarda la quantità di codice trattata dai test.

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

  • La copertura dei test misura in che misura i test coprono le stanze della casa.
  • La copertura del codice misura il numero di componenti della casa sottoposti a test.

Una copertura del codice del 100% non significa che non vi siano bug.

Sebbene sia sicuramente auspicabile raggiungere un'elevata copertura del codice durante i test, una copertura del codice del 100% non garantisce l'assenza di bug o difetti nel codice.

Un modo privo di significato per ottenere una copertura del codice del 100%

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 una copertura del 100% di funzioni, righe, ramo e istruzioni, ma non ha senso perché in realtà non testa il codice. L'asserzione expect(true).toBe(true) passerà sempre indipendentemente dal corretto funzionamento del codice.

Una metrica errata è peggiore di nessuna metrica

Una metrica errata può darti un falso senso di sicurezza, il che è peggiore di quanto non abbia una metrica. Ad esempio, se disponi di una suite di test che raggiunge una copertura del codice del 100%, 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 verranno comunque superati, 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 varietà di scenari diversi.
  • Utilizza la copertura del codice come linea guida e non come l'unica misura dell'efficacia del test o della qualità del codice.

Utilizzo della copertura del codice in diversi tipi di test

Diamo un'occhiata più da vicino a come puoi 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 diversi scenari e percorsi di test di piccole dimensioni.
  • Test di integrazione. Possono aiutare a raccogliere la copertura del codice per i test di integrazione, ma li utilizzano con cautela. In questo caso, viene calcolata la copertura di una porzione maggiore del codice sorgente ed è difficile determinare quali test coprono effettivamente determinate parti del codice. Tuttavia, il calcolo della copertura del codice dei test di integrazione può essere utile per i sistemi legacy che non hanno unità ben isolate.
  • Test end-to-end (E2E). Misurare la copertura del codice per i test E2E è difficile e complesso a causa della loro complessa natura. 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 il 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 cruciale 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.

Puntare a una copertura del codice del 100% non è l'obiettivo. Dovresti invece utilizzare la copertura del codice insieme a un piano di test completo che includa una varietà di metodi di test, tra cui test delle unità, test di integrazione, test end-to-end e test manuali.

Guarda l'esempio di codice completo ed esegui dei 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({});
  });
});