Python Admin

Guia Completo de Coverage em Python: pytest-cov, Relatórios e Metas de Cobertura Já leu

O que é Code Coverage e por que importa Code coverage, ou cobertura de código, é uma métrica que mede a porcentagem do seu código-fonte que é executada durante a execução de testes automatizados. Não se trata apenas de um número vangloriado; é uma ferramenta diagnóstica que revela pontos cegos em sua estratégia de testes. Quando você executa seu suite de testes, o coverage rastreia quais linhas, branches e funções foram realmente acionadas, mostrando exatamente onde há lacunas na validação do comportamento da aplicação. A relevância prática disso é imediata: código não testado é código que pode quebrar silenciosamente em produção. Uma cobertura alta não garante qualidade (você pode ter testes ruins que apenas "passam" pelo código), mas uma cobertura baixa é um sinal de alerta de que lógica crítica não está sendo validada. Em equipes de desenvolvimento maduras, o coverage é parte da definição de pronto para um pull request — sem atingir a meta de cobertura, o código

O que é Code Coverage e por que importa

Code coverage, ou cobertura de código, é uma métrica que mede a porcentagem do seu código-fonte que é executada durante a execução de testes automatizados. Não se trata apenas de um número vangloriado; é uma ferramenta diagnóstica que revela pontos cegos em sua estratégia de testes. Quando você executa seu suite de testes, o coverage rastreia quais linhas, branches e funções foram realmente acionadas, mostrando exatamente onde há lacunas na validação do comportamento da aplicação.

A relevância prática disso é imediata: código não testado é código que pode quebrar silenciosamente em produção. Uma cobertura alta não garante qualidade (você pode ter testes ruins que apenas "passam" pelo código), mas uma cobertura baixa é um sinal de alerta de que lógica crítica não está sendo validada. Em equipes de desenvolvimento maduras, o coverage é parte da definição de pronto para um pull request — sem atingir a meta de cobertura, o código não é aceito.

Instalação e Configuração Básica do pytest-cov

O pytest-cov é o plugin que integra medição de cobertura ao pytest. A instalação é trivial, mas a configuração adequada faz toda a diferença na utilidade prática da ferramenta.

Instalação e primeiro teste

Comece instalando o pacote via pip:

pip install pytest-cov

Agora crie um arquivo simples para testar. Vamos trabalhar com um módulo de utilitários matemáticos:

# calculadora.py
def somar(a, b):
    """Retorna a soma de dois números."""
    return a + b

def subtrair(a, b):
    """Retorna a subtração de dois números."""
    return a - b

def dividir(a, b):
    """Retorna a divisão de dois números. Levanta exceção se divisor é zero."""
    if b == 0:
        raise ValueError("Divisor não pode ser zero")
    return a / b

def multiplicar(a, b):
    """Retorna a multiplicação de dois números."""
    return a * b

Agora crie um arquivo de testes que não cobre todas as funções:

# test_calculadora.py
import pytest
from calculadora import somar, subtrair, dividir, multiplicar

def test_somar():
    assert somar(2, 3) == 5
    assert somar(-1, 1) == 0

def test_subtrair():
    assert subtrair(5, 3) == 2

def test_dividir_sucesso():
    assert dividir(10, 2) == 5.0

def test_dividir_erro():
    with pytest.raises(ValueError):
        dividir(10, 0)

# Note: multiplicar não tem teste!

Execute o pytest com o plugin pytest-cov:

pytest test_calculadora.py --cov=calculadora --cov-report=term-missing

A saída será algo assim:

Name              Stmts   Miss  Cover   Missing
-----------------------------------------------
calculadora.py        9      1    89%   18
-----------------------------------------------

A linha 18 é a função multiplicar, que nunca foi executada. O --cov-report=term-missing mostra exatamente quais linhas não foram cobertas.

Configuração via pytest.ini

Para não digitar flags toda vez, configure no arquivo pytest.ini (ou pyproject.toml):

[pytest]
addopts = --cov=calculadora --cov-report=term-missing --cov-report=html
testpaths = tests

Agora execute apenas pytest e a configuração é aplicada automaticamente. O --cov-report=html gera um relatório HTML bonito que você pode abrir no navegador.

Gerando Relatórios e Interpretando Resultados

Relatórios são mais que números; são histórias sobre quais partes do seu código estão bem testadas e quais precisam de atenção. pytest-cov oferece múltiplos formatos de saída, cada um servindo a um propósito diferente.

Tipos de relatórios

Relatório no Terminal (term-missing): É o mais rápido de visualizar durante desenvolvimento. Mostra um resumo tabular e, crucialmente, lista as linhas específicas não cobertas. Este é o que você consulta constantemente no seu workflow.

Relatório HTML: O mais visual e útil para apresentações e análise profunda. Gera um arquivo htmlcov/index.html que você abre no navegador, com código-fonte colorido: linhas verdes (cobertas), vermelhas (não cobertas) e amarelas (parcialmente cobertas em branches).

Relatório XML (Cobertura): Formato padrão da indústria, integrado com ferramentas de CI/CD como Jenkins, GitHub Actions e SonarQube. Permite que pipelines automatizados tomem decisões baseadas em métricas de cobertura.

Vamos expandir nosso exemplo para gerar todos os tipos:

pytest test_calculadora.py \
  --cov=calculadora \
  --cov-report=term-missing \
  --cov-report=html \
  --cov-report=xml

Após isso, você terá:
- Saída no terminal mostrando cada função e linha não coberta
- Diretório htmlcov/ pronto para ser aberto em navegador
- Arquivo coverage.xml para integração com ferramentas externas

Interpretando branches e linhas

Existe uma diferença crucial entre cobertura de linhas e cobertura de branches. Uma linha pode ser executada, mas nem todos os caminhos dentro dela. Considere:

# exemplo_branch.py
def validar_idade(idade, eh_cidadao):
    """Valida se pessoa pode votar."""
    if idade >= 18 and eh_cidadao:
        return "Pode votar"
    return "Não pode votar"

Um teste simples:

def test_validar_idade():
    assert validar_idade(20, True) == "Pode votar"

Isso cobre 100% das linhas, mas não 100% dos branches. A condição eh_cidadao nunca foi testada como False. Para verdadeiro branch coverage, você precisa:

def test_validar_idade_completo():
    assert validar_idade(20, True) == "Pode votar"
    assert validar_idade(17, True) == "Não pode votar"
    assert validar_idade(20, False) == "Não pode votar"

Ative branch coverage com:

pytest --cov=exemplo_branch --cov-branch --cov-report=term-missing

Definindo Metas de Cobertura e Automatizando Verificação

Estabelecer metas de cobertura é como definir limites de qualidade. Uma meta realista e defendida é muito mais valiosa que uma meta ambiciosa mas ignorada. A maioria das equipes maduras trabalha com 70-85% como alvo; acima de 90% geralmente indica testes supérfluos ou acoplamento excessivo.

Configurando limites de cobertura

Use o parâmetro --cov-fail-under para fazer o pytest falhar se a cobertura ficar abaixo da meta:

pytest --cov=calculadora --cov-fail-under=80

Se a cobertura for inferior a 80%, o teste falhará mesmo que todos os testes passem. Isso força a equipe a manter o padrão. Configure permanentemente no seu arquivo de configuração:

# pytest.ini
[pytest]
addopts = 
    --cov=calculadora
    --cov-report=term-missing
    --cov-fail-under=80
testpaths = tests

Metas granulares por módulo

Nem todos os módulos têm o mesmo nível de criticidade. Um módulo de utilitários pode ter 70% de cobertura aceitável, enquanto lógica de pagamento deve ter 95%. Use um arquivo .coveragerc para configurar metas específicas:

# .coveragerc
[run]
source = .
omit =
    */tests/*
    */venv/*
    setup.py

[report]
exclude_lines =
    pragma: no cover
    def __repr__
    raise AssertionError
    raise NotImplementedError
    if __name__ == .__main__.:
    if TYPE_CHECKING:

[coverage:report]
fail_under = 75

[coverage:paths]
source =
    calculadora
    */site-packages/calculadora

Linhas marcadas com pragma: no cover são explicitamente ignoradas. Use quando houver código legitimamente intestável:

def conectar_banco_dados():  # pragma: no cover
    # Código que só roda em produção
    conexao = criar_conexao_real()
    return conexao

Integração com GitHub Actions

Em um workflow real, você quer que a cobertura seja validada automaticamente em cada pull request. Aqui está um exemplo de configuração GitHub Actions:

# .github/workflows/tests.yml
name: Tests

on: [push, pull_request]

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3

      - name: Setup Python
        uses: actions/setup-python@v4
        with:
          python-version: '3.11'

      - name: Install dependencies
        run: |
          pip install pytest pytest-cov
          pip install -r requirements.txt

      - name: Run tests with coverage
        run: |
          pytest --cov=calculadora \
                  --cov-report=xml \
                  --cov-report=term-missing \
                  --cov-fail-under=80

      - name: Upload coverage to Codecov
        uses: codecov/codecov-action@v3
        with:
          file: ./coverage.xml

Agora, cada PR será automaticamente rejeitado se a cobertura cair abaixo de 80%. O Codecov fornece um badge bonito que você pode exibir no README.

Boas Práticas e Armadilhas Comuns

A cobertura é um meio, não um fim. Equipes iniciantes frequentemente caem em três armadilhas clássicas que você deve evitar desde o início.

A ilusão dos 100%

Atingir 100% de cobertura é matematicamente fácil — escreva um teste trivial para cada linha. Mas um teste que apenas passa pelo código sem validar comportamento não agrega valor. Considere:

# ruim.py
def processar_pagamento(valor, metodo):
    if metodo == "credito":
        return aplicar_taxa_credito(valor)
    elif metodo == "debito":
        return aplicar_taxa_debito(valor)
    else:
        raise ValueError("Método inválido")

# teste_ruim.py
def test_credito():
    assert processar_pagamento(100, "credito") == algo  # teste vazio

Esse teste cobre a linha, mas não valida o resultado correto. Uma abordagem melhor:

# melhor.py
def test_processar_pagamento_credito():
    resultado = processar_pagamento(100, "credito")
    # Validar o cálculo específico, não apenas que retorna algo
    assert resultado == 102.0  # 100 + 2% de taxa

def test_processar_pagamento_invalido():
    with pytest.raises(ValueError, match="Método inválido"):
        processar_pagamento(100, "bitcoin")

Qualidade de testes > quantidade de linhas cobertas.

Código intestável como sintoma

Se você está tendo dificuldade para atingir cobertura em um módulo, frequentemente o problema não é o teste — é o design do código. Código altamente acoplado, com dependências globais ou lógica misturada com I/O é difícil de testar. Use cobertura baixa como sinal para refatoração:

# ruim: lógica de negócio acoplada a I/O
def processar_vendas():
    conexao = mysql.connector.connect(host="localhost", user="root")
    cursor = conexao.cursor()
    cursor.execute("SELECT * FROM vendas WHERE status='pendente'")
    for venda in cursor.fetchall():
        if venda.valor > 1000:
            # lógica complexa aqui
            ...

# melhor: separação de responsabilidades
def processar_vendas(vendas_pendentes):
    """Processa lista de vendas. Fácil de testar com dados mock."""
    vendas_altas = filter(lambda v: v.valor > 1000, vendas_pendentes)
    return [aplicar_desconto(v) for v in vendas_altas]

# teste
def test_processar_vendas():
    vendas = [Venda(valor=500), Venda(valor=1500)]
    resultado = processar_vendas(vendas)
    assert len(resultado) == 1
    assert resultado[0].valor == 1500  # desconto não aplicado aqui, outro teste

Manutenção da cobertura ao longo do tempo

A cobertura tende a degradar conforme o projeto cresce. Configure alertas:

# Verificar se cobertura caiu em relação à baseline
pytest --cov=calculadora --cov-report=term-missing | tee coverage.txt

E no CI, compare com a execução anterior:

# No GitHub Actions, comentar no PR se cobertura diminuiu
- name: Comment PR with coverage
  if: github.event_name == 'pull_request'
  uses: py-cov-action/python-coverage-comment-action@v3

Conclusão

Você aprendeu que code coverage é uma métrica diagnóstica, não um objetivo em si — ela revela onde sua estratégia de testes tem lacunas, mas a qualidade dos testes é o que realmente importa. pytest-cov oferece múltiplos formatos de relatório (terminal, HTML, XML) que servem a públicos diferentes: desenvolvimento rápido, análise profunda e integração com ferramentas externas de CI/CD.

A automação de metas de cobertura via --cov-fail-under e configurações granulares no .coveragerc garantem que padrões de qualidade sejam mantidos consistentemente. Finalmente, use a cobertura como um espelho para identificar problemas de design — código com cobertura impossível de atingir geralmente indica acoplamento excessivo que merece refatoração.

Referências


Artigos relacionados