<h2>O que é Code Coverage e por que importa</h2>
<p>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.</p>
<p>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.</p>
<h2>Instalação e Configuração Básica do pytest-cov</h2>
<p>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.</p>
<h3>Instalação e primeiro teste</h3>
<p>Comece instalando o pacote via pip:</p>
<pre><code class="language-bash">pip install pytest-cov</code></pre>
<p>Agora crie um arquivo simples para testar. Vamos trabalhar com um módulo de utilitários matemáticos:</p>
<pre><code class="language-python"># 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</code></pre>
<p>Agora crie um arquivo de testes que <strong>não</strong> cobre todas as funções:</p>
<pre><code class="language-python"># 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!</code></pre>
<p>Execute o pytest com o plugin pytest-cov:</p>
<pre><code class="language-bash">pytest test_calculadora.py --cov=calculadora --cov-report=term-missing</code></pre>
<p>A saída será algo assim:</p>
<pre><code>Name Stmts Miss Cover Missing
-----------------------------------------------
calculadora.py 9 1 89% 18
-----------------------------------------------</code></pre>
<p>A linha 18 é a função <code>multiplicar</code>, que nunca foi executada. O <code>--cov-report=term-missing</code> mostra exatamente quais linhas não foram cobertas.</p>
<h3>Configuração via pytest.ini</h3>
<p>Para não digitar flags toda vez, configure no arquivo <code>pytest.ini</code> (ou <code>pyproject.toml</code>):</p>
<pre><code class="language-ini">[pytest]
addopts = --cov=calculadora --cov-report=term-missing --cov-report=html
testpaths = tests</code></pre>
<p>Agora execute apenas <code>pytest</code> e a configuração é aplicada automaticamente. O <code>--cov-report=html</code> gera um relatório HTML bonito que você pode abrir no navegador.</p>
<h2>Gerando Relatórios e Interpretando Resultados</h2>
<p>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.</p>
<h3>Tipos de relatórios</h3>
<p><strong>Relatório no Terminal (term-missing):</strong> É 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.</p>
<p><strong>Relatório HTML:</strong> O mais visual e útil para apresentações e análise profunda. Gera um arquivo <code>htmlcov/index.html</code> que você abre no navegador, com código-fonte colorido: linhas verdes (cobertas), vermelhas (não cobertas) e amarelas (parcialmente cobertas em branches).</p>
<p><strong>Relatório XML (Cobertura):</strong> 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.</p>
<p>Vamos expandir nosso exemplo para gerar todos os tipos:</p>
<pre><code class="language-bash">pytest test_calculadora.py \
--cov=calculadora \
--cov-report=term-missing \
--cov-report=html \
--cov-report=xml</code></pre>
<p>Após isso, você terá:</p>
<ul>
<li>Saída no terminal mostrando cada função e linha não coberta</li>
<li>Diretório <code>htmlcov/</code> pronto para ser aberto em navegador</li>
<li>Arquivo <code>coverage.xml</code> para integração com ferramentas externas</li>
</ul>
<h3>Interpretando branches e linhas</h3>
<p>Existe uma diferença crucial entre cobertura de <strong>linhas</strong> e cobertura de <strong>branches</strong>. Uma linha pode ser executada, mas nem todos os caminhos dentro dela. Considere:</p>
<pre><code class="language-python"># 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"</code></pre>
<p>Um teste simples:</p>
<pre><code class="language-python">def test_validar_idade():
assert validar_idade(20, True) == "Pode votar"</code></pre>
<p>Isso cobre 100% das <strong>linhas</strong>, mas não 100% dos <strong>branches</strong>. A condição <code>eh_cidadao</code> nunca foi testada como <code>False</code>. Para verdadeiro branch coverage, você precisa:</p>
<pre><code class="language-python">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"</code></pre>
<p>Ative branch coverage com:</p>
<pre><code class="language-bash">pytest --cov=exemplo_branch --cov-branch --cov-report=term-missing</code></pre>
<h2>Definindo Metas de Cobertura e Automatizando Verificação</h2>
<p>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.</p>
<h3>Configurando limites de cobertura</h3>
<p>Use o parâmetro <code>--cov-fail-under</code> para fazer o pytest falhar se a cobertura ficar abaixo da meta:</p>
<pre><code class="language-bash">pytest --cov=calculadora --cov-fail-under=80</code></pre>
<p>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:</p>
<pre><code class="language-ini"># pytest.ini
[pytest]
addopts =
--cov=calculadora
--cov-report=term-missing
--cov-fail-under=80
testpaths = tests</code></pre>
<h3>Metas granulares por módulo</h3>
<p>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 <code>.coveragerc</code> para configurar metas específicas:</p>
<pre><code class="language-ini"># .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</code></pre>
<p>Linhas marcadas com <code>pragma: no cover</code> são explicitamente ignoradas. Use quando houver código legitimamente intestável:</p>
<pre><code class="language-python">def conectar_banco_dados(): # pragma: no cover
Código que só roda em produção
conexao = criar_conexao_real()
return conexao</code></pre>
<h3>Integração com GitHub Actions</h3>
<p>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:</p>
<pre><code class="language-yaml"># .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</code></pre>
<p>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.</p>
<h2>Boas Práticas e Armadilhas Comuns</h2>
<p>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.</p>
<h3>A ilusão dos 100%</h3>
<p>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:</p>
<pre><code class="language-python"># 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</code></pre>
<p>Esse teste cobre a linha, mas não valida o resultado correto. Uma abordagem melhor:</p>
<pre><code class="language-python"># 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")</code></pre>
<p>Qualidade de testes > quantidade de linhas cobertas.</p>
<h3>Código intestável como sintoma</h3>
<p>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:</p>
<pre><code class="language-python"># 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</code></pre>
<h3>Manutenção da cobertura ao longo do tempo</h3>
<p>A cobertura tende a degradar conforme o projeto cresce. Configure alertas:</p>
<pre><code class="language-bash"># Verificar se cobertura caiu em relação à baseline
pytest --cov=calculadora --cov-report=term-missing | tee coverage.txt</code></pre>
<p>E no CI, compare com a execução anterior:</p>
<pre><code class="language-yaml"># 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</code></pre>
<h2>Conclusão</h2>
<p>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.</p>
<p>A automação de metas de cobertura via <code>--cov-fail-under</code> e configurações granulares no <code>.coveragerc</code> 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.</p>
<h2>Referências</h2>
<ul>
<li><a href="https://pytest-cov.readthedocs.io/" target="_blank" rel="noopener noreferrer">Pytest-cov Documentation</a></li>
<li><a href="https://coverage.readthedocs.io/" target="_blank" rel="noopener noreferrer">Coverage.py Official Documentation</a></li>
<li><a href="https://realpython.com/pytest-cov/" target="_blank" rel="noopener noreferrer">Real Python: Code Coverage in Python with pytest-cov</a></li>
<li><a href="https://martinfowler.com/bliki/CodeCoverage.html" target="_blank" rel="noopener noreferrer">Martin Fowler: Code Coverage</a></li>
<li><a href="https://github.com/codecov/codecov-action" target="_blank" rel="noopener noreferrer">GitHub Actions: Codecov Integration</a></li>
</ul>
<p><!-- FIM --></p>