Quatro tipos comuns de cobertura de código

Saiba o que é a cobertura de código e descubra quatro maneiras comuns de medi-la.

Você já ouviu a frase "cobertura de código"? Nesta postagem, vamos saber o que é a cobertura de código em testes e quatro maneiras comuns de medi-la.

O que é a cobertura de código?

A cobertura de código é uma métrica que mede a porcentagem do código-fonte que seus testes executam. Ajuda a identificar áreas que talvez não tenham testes adequados.

Geralmente, o registro dessas métricas é assim:

Arquivo % de instruções % de agências % de funções % de linhas Linhas descobertas
file.js 90% 100% 90% 80% 89.256
coffee.js 55,55% 80% 50% 62,5% 10 a 11 e 18 anos

À medida que você adiciona novos recursos e testes, aumentar as porcentagens de cobertura de código dá a você a confiança de que seu aplicativo foi completamente testado. No entanto, há mais para descobrir.

Quatro tipos comuns de cobertura de código

Há quatro maneiras comuns de coletar e calcular a cobertura de código: função, linha, ramificação e instrução.

Quatro tipos de cobertura de texto.

Para ver como cada tipo de cobertura de código calcula a porcentagem, considere o seguinte exemplo de código para calcular os ingredientes do 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);
}

Os testes que verificam a função calcCoffeeIngredient são:

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

Execute o código e os testes nesta demonstração ao vivo ou confira o repositório.

Cobertura da função

Cobertura de código: 50%

/* coffee.js */

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

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

A cobertura da função é uma métrica simples. Ele captura a porcentagem de funções no código que os testes chamam.

No exemplo de código, há duas funções: calcCoffeeIngredient e isValidCoffee. Os testes chamam apenas a função calcCoffeeIngredient. Portanto, a cobertura da função é de 50%.

Cobertura de linha

Cobertura de código: 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);
}

A cobertura de linha mede a porcentagem de linhas de código executáveis que seu conjunto de testes executou. Se uma linha de código permanecer não executada, isso significa que alguma parte dele não foi testada.

O exemplo de código tem oito linhas de código executável (destacadas em vermelho e verde), mas os testes não executam a condição americano (duas linhas) e a função isValidCoffee (uma linha). Isso resulta em uma cobertura de linha de 62,5%.

A cobertura de linha não considera as instruções de declaração, como function isValidCoffee(name) e let espresso, water;, porque elas não são executáveis.

Cobertura da agência

Cobertura de código: 80%

/* coffee.js */

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

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

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

  return {};
}
…

A cobertura da ramificação mede a porcentagem de ramificações ou pontos de decisão executados no código, como instruções "if" ou loops. Ela determina se os testes examinam as ramificações verdadeiras e falsas das instruções condicionais.

Há cinco ramificações no exemplo de código:

  1. Ligando para calcCoffeeIngredient com apenas coffeeName Marca chek.
  2. Ligando para calcCoffeeIngredient com coffeeName e cup Marca chek.
  3. Café espresso Marca chek.
  4. O café é americano X.
  5. Outro café Marca chek.

Os testes abrangem todas as ramificações, exceto a condição Coffee is Americano. Assim, a cobertura da filial é de 80%.

Cobertura do extrato

Cobertura de código: 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);
}

A cobertura de instruções mede a porcentagem de instruções no código que os testes executam. À primeira vista, você pode se perguntar, “isso não é o mesmo que cobertura de linha?” De fato, a cobertura de declaração é semelhante à cobertura de linha, mas leva em consideração linhas de código que contêm várias instruções.

No exemplo de código, há oito linhas de código executável, mas há nove instruções. Você consegue identificar a linha que contém duas declarações?

Conferir sua resposta

É a seguinte linha: espresso = 30 * cup; water = 70 * cup;

Os testes abrangem apenas cinco das nove instruções, portanto, a cobertura da instrução é de 55,55%.

Se você sempre redigir um extrato por linha, sua cobertura de linha será semelhante à cobertura do seu extrato.

Que tipo de cobertura de código você deve escolher?

A maioria das ferramentas de cobertura de código inclui esses quatro tipos comuns de cobertura de código. A escolha da métrica de cobertura de código a ser priorizada depende dos requisitos específicos do projeto, das práticas de desenvolvimento e das metas de teste.

Em geral, a cobertura da declaração é um bom ponto de partida porque é uma métrica simples e fácil de entender. Ao contrário da cobertura de instruções, a cobertura de ramificações e a cobertura de função medem se os testes chamam uma condição (ramificação) ou função. Portanto, elas são uma progressão natural após a cobertura da declaração.

Depois de atingir uma alta cobertura de extratos, você pode passar para a cobertura de agências e funções.

A cobertura do teste é igual à cobertura do código?

Não. Muitas vezes, a cobertura de teste e a cobertura de código são confundidas, mas são diferentes:

  • Cobertura do teste: métrica qualitativa que mede o quanto o pacote de testes cobre os recursos do software. Ele ajuda a determinar o nível de risco envolvido.
  • Cobertura de código: é uma métrica quantitativa que mede a proporção do código executado durante o teste. Trata-se da quantidade de código que os testes abrangem.

Veja aqui uma analogia simplificada: imagine um aplicativo da Web como uma casa.

  • A cobertura de teste mede o desempenho dos testes em todos os cômodos da casa.
  • A cobertura de código mede quanto do ambiente os testes passaram.

100% de cobertura de código não significa que não há bugs

Embora certamente seja desejável alcançar uma alta cobertura de código nos testes, 100% de cobertura de código não garante a ausência de bugs ou falhas no seu código.

Uma maneira irrelevante de atingir 100% de cobertura de código

Considere o seguinte teste:

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

Esse teste atinge 100% de cobertura de função, linha, ramificação e instrução, mas não faz sentido porque não testa o código. A declaração expect(true).toBe(true) sempre será transmitida, independente do código funcionar corretamente.

Uma métrica ruim é pior do que nenhuma métrica

Uma métrica ruim pode dar a você uma falsa sensação de segurança, o que é pior do que não ter nenhuma métrica. Por exemplo, se você tem um pacote de testes que atinge 100% de cobertura de código, mas os testes não têm significado, você pode ter uma falsa sensação de segurança de que o código foi bem testado. Se você acidentalmente excluir ou quebrar uma parte do código do aplicativo, os testes ainda serão aprovados, mesmo que o aplicativo não funcione mais corretamente.

Para evitar esse cenário:

  • Revisão de teste. Escrever e revisar testes para garantir que eles sejam significativos e testar o código em vários cenários diferentes.
  • Use a cobertura do código como orientação, não como a única medida de eficácia de teste ou qualidade do código.

Como usar cobertura de código em diferentes tipos de testes

Vamos analisar como a cobertura de código pode ser usada com os três tipos comuns de teste:

  • Testes de unidade. Eles são o melhor tipo de teste para coletar cobertura de código, porque são projetados para abranger vários cenários pequenos e caminhos de teste.
  • Testes de integração. Eles podem ajudar a coletar cobertura de código para testes de integração, mas os usam com cuidado. Nesse caso, você calcula a cobertura de uma parte maior do código-fonte, e pode ser difícil determinar quais testes realmente cobrem quais partes do código. No entanto, calcular a cobertura de código dos testes de integração pode ser útil para sistemas legados que não têm unidades bem isoladas.
  • Testes completos (E2E). Medir a cobertura de código em testes E2E é difícil e desafiador devido à natureza complexa desses testes. Em vez de usar a cobertura de código, a cobertura de requisitos pode ser a melhor solução. Isso ocorre porque o foco dos testes E2E é cobrir os requisitos do teste, não se concentrar no código-fonte.

Conclusão

A cobertura de código pode ser uma métrica útil para medir a eficácia dos seus testes. Ela pode ajudar a melhorar a qualidade do aplicativo, garantindo que a lógica crucial do código seja bem testada.

No entanto, lembre-se de que a cobertura de código é apenas uma métrica. Considere também outros fatores, como a qualidade dos testes e os requisitos do aplicativo.

Alcançar 100% de cobertura do código não é o objetivo. Em vez disso, use a cobertura de código com um plano completo que incorpore vários métodos de teste, incluindo testes de unidade, de integração, completos e manuais.

Confira o exemplo de código completo e os testes com boa cobertura de código. Você também pode executar o código e os testes com esta demonstração ao 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({});
  });
});