Aprenda 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 abordar 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 do código é uma métrica que mede a porcentagem de código-fonte que seus testes executam. Ajuda a identificar áreas que podem não ter testes adequados.
Geralmente, o registro dessas métricas é semelhante ao seguinte:
Arquivo | % de declarações | % de ramificação | Funções% | % linhas | Linhas descobertas |
---|---|---|---|---|---|
file.js | 90% | 100% | 90% | 80% | 89.256 |
coffee.js | 55,55% | 80% | 50% | 62,5% | 11 a 10 de 18 |
À medida que você adiciona novos recursos e testes, aumentar as porcentagens de cobertura de código aumenta a confiança de que o aplicativo foi completamente testado. No entanto, ainda há muito o que descobrir.
Quatro tipos comuns de cobertura de código
Há quatro maneiras comuns de coletar e calcular a cobertura de código: cobertura de função, linha, ramificação e instrução.
Para conferir como cada tipo de cobertura de código calcula a porcentagem, considere o seguinte exemplo de código para calcular os ingredientes de 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({});
});
});
É possível executar o código e os testes nesta demonstração ao vivo ou conferir 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 direta. Ela 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 linhas
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 não for executada, isso significa que alguma parte do código 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 filial
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 de ramificação mede a porcentagem de ramificações ou pontos de decisão executados no código, como instruções if ou loops. Determina se os testes examinam as ramificações verdadeiras e falsas de declarações condicionais.
Há cinco ramificações no exemplo de código:
- Ligando para
calcCoffeeIngredient
com apenascoffeeName
- Ligando para
calcCoffeeIngredient
comcoffeeName
ecup
- Café é expresso
- O café é Americano
- Outro café
Os testes abrangem todas as ramificações, exceto a condição Coffee is Americano
. Então, a cobertura das agências é 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 a cobertura de linha?” Na verdade, a cobertura da instrução é semelhante à da linha, mas leva em consideração linhas únicas de código que contêm várias instruções.
No exemplo do código, há oito linhas de código executável, mas nove instruções. Você consegue identificar a linha que contém duas declarações?
espresso = 30 * cup; water = 70 * cup;
Os testes abrangem apenas cinco das nove afirmações, portanto, a cobertura da instrução é de 55,55%.
Se você sempre escrever uma declaração por linha, a cobertura da sua 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 estes quatro tipos comuns de cobertura de código. A escolha da métrica de cobertura de código a ser priorizada depende de requisitos específicos do projeto, práticas de desenvolvimento e 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 da instrução, a cobertura da ramificação e da função medem se os testes chamam uma condição (ramificação) ou uma função. Portanto, elas são uma progressão natural após a cobertura da declaração.
Depois de atingir uma alta cobertura de instruções, você pode passar para a cobertura da filial e da função.
A cobertura de teste é igual à cobertura de código?
Não. As coberturas de teste e de código costumam ser confundidas, mas são diferentes:
- Cobertura de teste: métrica qualitativa que mede o quanto o pacote de testes abrange 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. Ela diz respeito à quantidade de código que os testes abrangem.
Aqui está uma analogia simplificada: imagine um aplicativo da Web como uma casa.
- A cobertura de teste mede o quanto os testes cobrem os ambientes da casa.
- A cobertura de código mede quanto da casa os testes passaram.
Ter 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 em seu código.
Uma maneira sem sentido 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 vai ser transmitida, independente do código funcionar corretamente.
Uma métrica ruim é pior do que nenhuma métrica
Uma métrica ruim pode dar 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 com 100% de cobertura de código, mas todos os testes são irrelevantes, isso pode gerar uma falsa sensação de segurança de que o código foi bem testado. Se você acidentalmente excluir ou corromper 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, faça o seguinte:
- Análise de teste. Programe e revise os testes para garantir que sejam significativos e teste o código em várias situações diferentes.
- Use a cobertura do código como uma diretriz, não como a única medida de eficácia do teste ou qualidade do código.
Como usar a cobertura de código em diferentes tipos de teste
Confira mais detalhes sobre como usar a cobertura do código 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 cobrir vários cenários pequenos e caminhos de teste.
- Testes de integração. Elas podem ajudar a coletar a cobertura de código para testes de integração, mas precisam ser usadas com cautela. Nesse caso, você calcula a cobertura de uma parte maior do código-fonte, e pode ser difícil determinar quais testes realmente abrangem quais partes do código. No entanto, calcular a cobertura do código dos testes de integração pode ser útil para sistemas legados que não têm unidades bem isoladas.
- Testes de ponta a ponta (E2E). Medir a cobertura do código para 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 opção. Isso ocorre porque o foco dos testes E2E é cobrir os requisitos do teste, e não se concentrar no código-fonte.
Conclusão
A cobertura do código pode ser uma métrica útil para medir a eficácia dos seus testes. Ela pode ajudar você a melhorar a qualidade do seu 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.
Não ter 100% de cobertura de código é o objetivo. Em vez disso, você deve usar a cobertura de código com um plano de testes completo que incorpore uma variedade de métodos de teste, incluindo testes de unidade, testes de integração, testes de ponta a ponta e testes manuais.
Confira o exemplo de código completo e os testes com boa cobertura de código. Também é possível 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({});
});
});