Você tem 90% de code coverage e ainda foi acionado às 2 da manhã.

Os testes unitários passaram. O CI estava verde. O bug chegou à produção mesmo assim. O coverage não mentiu, mas também não contou a verdade. Ele mediu quais linhas foram executadas, não quais comportamentos foram realmente verificados.

A maioria das equipes descobre isso da maneira mais difícil. Elas escrevem centenas de testes unitários, veem o badge de coverage ficar verde e assumem que a fortaleza está segura. A fortaleza tem paredes. Só não tem teto.

Testes Unitários Só Testam o Que Você Imagina Que Pode Dar Errado

Testes unitários validam suas suposições sobre seu código. O problema é que bugs não se importam com suas suposições.

Considere uma função de preços simples:

def calculate_total(items, tax_rate):
    subtotal = sum(item["price"] * item["quantity"] for item in items)
    tax = subtotal * tax_rate
    return round(subtotal + tax, 2)

Um suite típico de testes unitários parece sólido:

def test_calculate_total_with_tax():
    items = [{"price": 10.00, "quantity": 2}]
    assert calculate_total(items, 0.08) == 21.60

def test_calculate_total_empty_cart():
    assert calculate_total([], 0.08) == 0.00

Ambos passam. O coverage é 100%. A função vai para produção.

Então um cliente no Japão finaliza a compra com três itens precificados em ¥100, ¥100 e ¥100. A taxa de imposto é 0,10. O total esperado é ¥330. A função retorna ¥330,00. Tudo bem.

Um cliente na Suíça compra um item a CHF 12,35 com 7,7% de VAT. Esperado: CHF 13,30. Real: CHF 13,30. Ainda bem.

Então um cliente compra dois itens a $0,01 cada em Oregon, onde a taxa de imposto é 0,0. Esperado: $0,02. Real: $0,02. Passa.

O bug aparece quando um cliente em uma jurisdição com uma taxa de imposto None (porque o serviço de impostos retornou um null para um zip code não reconhecido) tenta finalizar a compra. A função multiplica subtotal * None e lança um TypeError. Seus testes unitários nunca passaram None como taxa de imposto porque você assumiu que sempre seria um float.

Esta é a limitação fundamental. Testes unitários exercitam os caminhos que você pensou em testar. Bugs vivem nos caminhos que você não pensou.

Os Quatro Lugares Onde Testes Unitários Não Alcançam

Limites de Integração

Testes unitários substituem dependências externas por mocks. Mocks são educados. Eles fazem exatamente o que você diz. APIs reais não são educadas.

Seu mock de banco de dados retorna linhas em milissegundos. A produção retorna em segundos, ou dá timeout, ou retorna linhas duplicadas por causa de um lag de read replica que você não sabia que existia.

Seu mock de cliente HTTP retorna JSON limpo. O serviço real retorna um 200 com um body vazio às terças-feiras.

Mocks testam seu código contra suas suposições sobre outros sistemas. A produção testa seu código contra a realidade. São suites de teste diferentes com taxas de aprovação diferentes.

Bugs de Estado e Temporais

Testes unitários rodam isolados. Cada teste recebe um estado novo. A produção é um processo de longa duração onde o estado se acumula, vaza e interage consigo mesmo.

Um cache de memória que expulsa entradas sob carga. Um pool de conexões que se esgota após 10.000 requisições. Uma comparação de timestamp que falha quando o teste roda através de uma mudança de horário de verão. Esses bugs exigem tempo, volume ou sequência para se manifestarem. Testes unitários não têm nenhum desses.

Concorrência e race conditions

Dois usuários atualizam o mesmo registro simultaneamente. Uma requisição lê um saldo, outra debita, a primeira escreve de volta o valor desatualizado. Dinheiro some. Seus testes unitários rodam sequencialmente em uma única thread. Eles não conseguem capturar isso.

Você pode escrever testes unitários para primitivas de lock individuais. Você não pode escrever um teste unitário que prove que seu sistema inteiro está livre de race conditions. O espaço de estado é grande demais e o timing é muito não-determinístico.

O Próprio Ambiente

Seus testes rodam no Ubuntu 22.04 com Python 3.11, 4 GB de RAM e sem regras de firewall. A produção roda no Alpine Linux com Python 3.11, 512 MB de RAM e um security group que dropa conexões TCP idle após 60 segundos.

O module socket se comporta de forma diferente. Os limites de mmap são menores. As configurações de locale fazem o strftime formatar datas de formas que seu parser não espera. Esses não são bugs de código. São bugs de contexto. Testes unitários não têm contexto.

Por Que a Porcentagem de Coverage Engana

Ferramentas de coverage medem execução de linhas, não qualidade de asserções. Um teste pode executar cada linha de uma função e não verificar nada significativo.

def test_poor_coverage_quality():
    result = calculate_total([{"price": 1.0, "quantity": 1}], 0.0)
    # Executed 100% of lines. Verified almost nothing.
    assert result is not None

Esse teste te dá 100% de line coverage e zero confiança. Muitas equipes otimizam a metric porque é fácil de medir. Confiança é difícil de medir. Então elas medem coverage em vez disso e esperam que as duas se correlacionem.

Elas não se correlacionam.

O Que Testar em Vez Disso (Ou Além Disso)

Isso não é um argumento contra testes unitários. Testes unitários são rápidos, determinísticos e excelentes para verificar lógica algorítmica. Eles são apenas incompletos.

Aqui está o que preenche as lacunas sem transformar sua pipeline de CI em um passivo de 45 minutos.

Teste nos Limites do Sistema, Não Apenas Internamente

Em vez de fazer mock do banco de dados, escreva testes que atinjam um banco de dados de teste real. Eles são mais lentos, então execute-os seletivamente. Mas eles capturam o mismatch entre suas queries de ORM e o comportamento real do query planner.

Em vez de fazer mock do cliente HTTP, suba o serviço downstream em um container. Isso captura schema drift, comportamento de timeout e lógica de retry que só dispara em falhas reais de conexão.

Adicione Contract Tests para Serviços Externos

Se você não pode rodar a dependência real no CI, use contract tests. Eles verificam se as expectativas do seu consumer correspondem ao schema real da API do provider.

Ferramentas como Pact gravam as interações entre seu serviço e suas dependências. Se o provider muda o tipo de um campo ou remove um endpoint, o contract test falha antes do código ser deployado. Não é tão bom quanto integration testing, mas é muito melhor do que torcer para que seus mocks estejam precisos.

Use Property-Based Testing para Casos de Borda

Ferramentas de property-based testing como Hypothesis (Python) ou fast-check (JavaScript) geram milhares de inputs aleatórios e verificam se seus invariants se mantêm.

from hypothesis import given, strategies as st

@given(
    st.lists(st.fixed_dictionaries({
        "price": st.decimals(min_value=0, max_value=10000, places=2),
        "quantity": st.integers(min_value=0, max_value=1000)
    })),
    st.one_of(st.none(), st.decimals(min_value=0, max_value=1, places=4))
)
def test_calculate_total_invariants(items, tax_rate):
    if tax_rate is None:
        with pytest.raises(TypeError):
            calculate_total(items, tax_rate)
        return

    result = calculate_total(items, tax_rate)
    assert result >= 0
    assert result == round(result, 2)

Esse teste teria capturado o bug de taxa de imposto None sem você precisar pensar em escrever aquele caso específico. Ele gera inputs que você nunca consideraria: listas vazias, listas gigantes, preços zero, decimais de precisão máxima. Ele encontra as bordas da sua lógica sem exigir que você as imagine primeiro.

Monitore a Produção Como Se Fosse um Ambiente de Teste

O suite de teste mais honesto é o tráfego de produção. Se você não consegue capturar um bug antes de ele ir para produção, capture-o antes que ele cause dano.

Use feature flags para lançar mudanças para 1% dos usuários primeiro. Observe error rates, latency percentiles e metrics de negócio. Um teste unitário te diz se o código se comporta como esperado isoladamente. Um monitor de produção te diz se o código se comporta como esperado na realidade.

Configure alerts sobre anomalias, não apenas falhas duras. Um aumento de 5% em erros 500 após um deploy é frequentemente o único sinal de que uma race condition ou resource leak começou. Testes unitários nunca vão te mostrar isso.

O Trade-off Honesto

Testes unitários são baratos, rápidos e bons para loops de feedback do desenvolvedor. integration tests são caros, lentos e bons para capturar os bugs que importam.

Você precisa dos dois. A armadilha é pensar que 100% de coverage de testes unitários significa que você pode pular o resto. Significa que você testou as partes fáceis a fundo. As partes difíceis, aquelas que te acordam à noite, vivem onde seus testes não estão olhando.

Comece com testes unitários para lógica e algoritmos. Adicione integration tests em cada limite do sistema. Use property-based testing para encontrar os inputs que você não pensou. Monitore a produção para capturar o que cada teste deixou passar.

Coverage é uma vanity metric. A única metric que importa é se você dorme a noite toda.


Se você está tentando capturar os bugs que seus testes deixam passar, comece olhando seus dados de erro. Sentry mostra o que quebra em produção, com as stack traces e o contexto que seus testes unitários nunca tiveram.