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.