Python

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

15 min de leitura

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

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

<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 &quot;passam&quot; 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):

&quot;&quot;&quot;Retorna a soma de dois números.&quot;&quot;&quot;

return a + b

def subtrair(a, b):

&quot;&quot;&quot;Retorna a subtração de dois números.&quot;&quot;&quot;

return a - b

def dividir(a, b):

&quot;&quot;&quot;Retorna a divisão de dois números. Levanta exceção se divisor é zero.&quot;&quot;&quot;

if b == 0:

raise ValueError(&quot;Divisor não pode ser zero&quot;)

return a / b

def multiplicar(a, b):

&quot;&quot;&quot;Retorna a multiplicação de dois números.&quot;&quot;&quot;

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):

&quot;&quot;&quot;Valida se pessoa pode votar.&quot;&quot;&quot;

if idade &gt;= 18 and eh_cidadao:

return &quot;Pode votar&quot;

return &quot;Não pode votar&quot;</code></pre>

<p>Um teste simples:</p>

<pre><code class="language-python">def test_validar_idade():

assert validar_idade(20, True) == &quot;Pode votar&quot;</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) == &quot;Pode votar&quot;

assert validar_idade(17, True) == &quot;Não pode votar&quot;

assert validar_idade(20, False) == &quot;Não pode votar&quot;</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: &#039;3.11&#039;

  • 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 == &quot;credito&quot;:

return aplicar_taxa_credito(valor)

elif metodo == &quot;debito&quot;:

return aplicar_taxa_debito(valor)

else:

raise ValueError(&quot;Método inválido&quot;)

teste_ruim.py

def test_credito():

assert processar_pagamento(100, &quot;credito&quot;) == 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, &quot;credito&quot;)

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=&quot;Método inválido&quot;):

processar_pagamento(100, &quot;bitcoin&quot;)</code></pre>

<p>Qualidade de testes &gt; 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=&quot;localhost&quot;, user=&quot;root&quot;)

cursor = conexao.cursor()

cursor.execute(&quot;SELECT * FROM vendas WHERE status=&#039;pendente&#039;&quot;)

for venda in cursor.fetchall():

if venda.valor &gt; 1000:

lógica complexa aqui

...

melhor: separação de responsabilidades

def processar_vendas(vendas_pendentes):

&quot;&quot;&quot;Processa lista de vendas. Fácil de testar com dados mock.&quot;&quot;&quot;

vendas_altas = filter(lambda v: v.valor &gt; 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 == &#039;pull_request&#039;

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>&lt;!-- FIM --&gt;</p>

Comentários

Mais em Python

Boas Práticas de Estruturas de Controle em Python: if, match-case e Expressões para Times Ágeis
Boas Práticas de Estruturas de Controle em Python: if, match-case e Expressões para Times Ágeis

Introdução: O Controle de Fluxo como Base da Programação As estruturas de con...

Django REST Framework: Serializers, ViewSets e Autenticação: Do Básico ao Avançado
Django REST Framework: Serializers, ViewSets e Autenticação: Do Básico ao Avançado

Serializers: A Base da Transformação de Dados Um serializer no Django REST Fr...

Mocks em Python: unittest.mock, patch e pytest-mock na Prática: Do Básico ao Avançado
Mocks em Python: unittest.mock, patch e pytest-mock na Prática: Do Básico ao Avançado

Introdução: Por que Mocks são Essenciais Quando desenvolvemos software profis...