Cobertura de código: a métrica importa realmente

1. O que é cobertura de código e como ela é medida?

Cobertura de código é uma métrica que indica qual porcentagem do código-fonte foi executada durante a execução dos testes. Existem diferentes tipos de cobertura:

  • Cobertura de linhas: quantas linhas de código foram executadas
  • Cobertura de branches: quantos caminhos condicionais (if/else, switch) foram percorridos
  • Cobertura de funções: quantas funções/métodos foram chamados
  • Cobertura de caminhos: quantas combinações possíveis de execução foram testadas

Ferramentas populares incluem coverage.py (Python), Istanbul (JavaScript) e JaCoCo (Java). Um relatório básico de cobertura se parece com:

Name                    Stmts   Miss  Cover
-------------------------------------------
calculadora.py            20      2    90%
validador_cpf.py          15      5    67%
processador_pagamento.py  30      0   100%
-------------------------------------------
TOTAL                     65      7    89%

2. A armadilha da cobertura cega

A cobertura cria uma falsa sensação de segurança. É perfeitamente possível ter 100% de cobertura com testes inúteis. Veja um exemplo real:

# Código sendo testado
def calcular_desconto(valor, cliente_vip):
    if cliente_vip:
        return valor * 0.9
    return valor * 0.95

# Teste que atinge 100% de cobertura mas não valida nada
def test_calcular_desconto():
    calcular_desconto(100, True)    # Executa, mas não verifica resultado
    calcular_desconto(100, False)   # Executa, mas não verifica resultado
    # Nenhum assert! Cobertura = 100%, teste = inútil

Casos reais mostram equipes celebrando 95% de cobertura enquanto bugs críticos passam despercebidos — porque os testes não verificavam condições de borda, valores negativos ou entradas nulas.

3. Métricas complementares que importam mais

Mutação de código (mutation testing): essa técnica insere pequenas modificações no código (mutantes) e verifica se os testes os detectam. Se um mutante sobrevive, seus testes são frágeis.

# Código original
if idade >= 18:
    return "adulto"

# Mutante gerado
if idade > 18:    # mudança de >= para >
    return "adulto"

# Se o teste não detectar essa mudança, o mutante sobreviveu

Complexidade ciclomática: mede quantos caminhos independentes existem no código. Um módulo com cobertura de 90% mas complexidade 50 tem muito mais risco que um com 70% e complexidade 5.

Taxa de falhas em produção: estudos mostram que correlação entre cobertura alta e baixa taxa de bugs é fraca. O que realmente importa é se os testes cobrem cenários reais de uso.

4. Estratégias para usar cobertura de forma inteligente

Defina metas realistas por módulo:

# Configuração de cobertura por tipo de módulo
Regras sugeridas:
- Lógica de negócio (domínio): 80-90% de cobertura de branches
- Repositórios/DAO: 70-80% de cobertura de linhas
- Interface de usuário: 50-60% (testes E2E complementam)
- Configuração/bootstrapping: 30-40% (código boilerplate)

Na integração contínua, evite metas fixas que bloqueiam deploys. Prefira alertar apenas em quedas drásticas:

# Estratégia de CI: falhar apenas se cobertura cair >5%
# Exemplo com GitHub Actions
- name: Check coverage drop
  run: |
    CURRENT=85
    PREVIOUS=$(cat coverage_history.txt)
    if [ $((PREVIOUS - CURRENT)) -gt 5 ]; then
      echo "Cobertura caiu mais de 5%!"
      exit 1
    fi

5. Cobertura em diferentes tipos de teste

Testes unitários, de integração e E2E contribuem de forma diferente para a cobertura:

# Teste unitário: cobre lógica isolada (alta cobertura de branches)
def test_calcular_imposto():
    assert calcular_imposto(1000) == 150

# Teste de integração: cobre fluxo entre componentes
def test_fluxo_pedido():
    resposta = cliente.post("/pedido", json={"item": "livro"})
    assert resposta.status_code == 201

# Teste E2E: cobre caminhos completos (baixa cobertura de código)
def test_compra_completa():
    page.goto("https://loja.com")
    page.click("button#comprar")
    assert page.text_content(".mensagem") == "Compra realizada"

Testes parametrizados com pytest aumentam a cobertura de caminhos sem duplicar código:

import pytest

@pytest.mark.parametrize("idade,esperado", [
    (17, "menor"),
    (18, "adulto"),
    (65, "idoso"),
    (-1, "inválido"),   # borda
    (0, "inválido"),    # borda
])
def test_classificar_idade(idade, esperado):
    assert classificar_idade(idade) == esperado

6. Mitos comuns sobre cobertura de código

Mito 1: "100% de cobertura = código perfeito" — Falso. Cobertura não mede qualidade das asserções, performance, segurança ou usabilidade.

Mito 2: "Cobertura alta dispensa testes manuais ou de aceitação" — Falso. Testes automatizados não capturam problemas de UX, fluxos visuais ou requisitos não funcionais.

Mito 3: "Só times iniciantes se preocupam com cobertura" — Falso. Empresas como Google e Microsoft usam cobertura como métrica auxiliar, mas combinam com code review, testes de mutação e análise estática.

7. Quando a cobertura realmente importa

Em projetos com requisitos críticos, a cobertura funciona como rede de proteção:

  • Saúde e finanças: sistemas de prescrição médica, processamento de pagamentos
  • Código legado em refatoração: cobertura alta permite refatorar com confiança
  • Equipes grandes: cobertura funciona como padrão mínimo de qualidade e comunicação entre desenvolvedores
# Exemplo: refatoração segura com cobertura
# Antes (cobertura 90%)
def processar_transacao(valor, tipo):
    if tipo == "credito":
        return valor + taxa
    elif tipo == "debito":
        return valor - taxa

# Depois da refatoração (cobertura mantida em 90%)
def processar_transacao(valor, tipo):
    operacoes = {"credito": lambda v: v + taxa, "debito": lambda v: v - taxa}
    return operacoes.get(tipo, lambda v: 0)(valor)

8. Conclusão prática: cobertura como guia, não como deus

Cobertura de código é uma métrica útil, mas insuficiente sozinha. Ela responde "o código foi executado?", mas não "o código foi testado adequadamente?".

Recomendações finais:

  1. Combine métricas: cobertura + mutação + complexidade ciclomática
  2. Automatize a medição: gere relatórios em cada PR e revise periodicamente
  3. Foque em branches, não apenas linhas: 80% de cobertura de branches vale mais que 95% de linhas
  4. Use cobertura para identificar código não testado, não como meta de desempenho
  5. Invista em testes de mutação para validar a qualidade real dos testes

Lembre-se: cobertura alta com testes fracos é pior que cobertura média com testes robustos. A métrica deve guiar, não ditar as regras.

Referências