Pular para o conteúdo
10 min de leitura

A pirâmide de testes: por que testar rápido e testar o suficiente não são a mesma coisa

Por Equipe Tech do Sonne ·

Unitários, de integração e end-to-end: entenda a pirâmide de testes, o que cada camada prova, e como montar uma suíte que dá confiança sem virar um peso morto.

Neste artigo

Testes existem para deixar você mudar o código com confiança#

Há uma pergunta que expõe a maturidade de qualquer equipe de engenharia: quando um teste passa, o que exatamente isso prova? A resposta ingênua é "que o código funciona". A resposta útil é mais modesta e mais poderosa: prova que um comportamento específico continua valendo depois de uma mudança. Testes não são um selo de qualidade absoluto; são uma rede de segurança que permite alterar o sistema sem rezar. O valor de uma suíte de testes não está em ela existir, mas em quanto ela reduz o medo de mexer no código.

Esse enquadramento muda tudo. Se o objetivo é confiança para mudar, então a métrica que importa não é "quantos testes temos" nem "qual a porcentagem de cobertura", mas: quando eu quebrar algo importante, um teste falha? E quando um teste falha, ele me aponta com precisão o que quebrou? Uma suíte que passa verde enquanto o sistema está com um bug real é pior que inútil — dá confiança falsa. Uma suíte que falha por qualquer mudança trivial, sem apontar defeito real, é um imposto que a equipe aprende a ignorar.

A pirâmide e a lógica por trás dela#

A metáfora da pirâmide de testes, popularizada há mais de uma década, continua sendo o mapa mais útil para pensar a distribuição de uma suíte. Ela propõe três camadas, com quantidades decrescentes conforme se sobe:

  • Testes unitários (base larga): rápidos, numerosos, isolam uma unidade pequena de lógica.
  • Testes de integração (meio): verificam que peças conversam corretamente — o código com o banco, um serviço com outro.
  • Testes end-to-end (topo estreito): exercitam o sistema inteiro pela interface real do usuário, do clique à resposta.

A forma piramidal não é arbitrária. Ela reflete um trade-off fundamental entre três propriedades que puxam em direções opostas: velocidade, realismo e precisão do diagnóstico. Conforme se sobe, os testes ficam mais realistas (exercitam mais o sistema como ele roda de verdade) mas também mais lentos, mais frágeis e mais vagos quando falham. Um teste unitário roda em milissegundos e, quando quebra, aponta a função culpada. Um teste end-to-end pode levar minutos, quebrar por um problema de rede, e quando falha só diz "a compra não concluiu" — sem dizer em qual das quarenta camadas envolvidas está o defeito.

A pirâmide sugere: tenha muitos testes baratos e precisos na base, alguns testes de integração no meio, e pouquíssimos testes de ponta a ponta cobrindo os fluxos críticos. O anti-padrão oposto — o "cone de sorvete", com muitos testes end-to-end e poucos unitários — produz suítes lentas, instáveis e que ninguém confia. Toda equipe que reclama que "os testes ficam vermelhos aleatoriamente" e que "a suíte demora quarenta minutos" está, quase sempre, com a pirâmide invertida.

O que faz um teste unitário ser bom#

A base da pirâmide carrega o maior peso, então vale saber o que separa um bom teste unitário de um ruim. O melhor teste unitário verifica um comportamento observável através da interface pública da unidade, não os detalhes de sua implementação.

Essa distinção é sutil e decisiva. Um teste que verifica "quando eu deposito 100 e depois saco 30, o saldo é 70" testa comportamento — ele continua válido mesmo que você reescreva completamente a classe por dentro. Um teste que verifica "o método depositar chama this.repositorio.salvar exatamente uma vez com estes argumentos" testa implementação — ele quebra assim que você refatora, mesmo que o comportamento continue perfeito. Testes acoplados à implementação transformam a suíte de rede de segurança em camisa de força: você deixa de refatorar porque cada mudança interna dispara uma cascata de testes vermelhos que não indicam bug nenhum.

Um bom teste unitário também é honesto sobre o que assere. O anti-padrão do "verde mentiroso" é o teste que sempre passa: aquele que verifica apenas que a função não lançou exceção, ou que assere resultado != null sem olhar o conteúdo. Ele infla a cobertura e não prova nada. Um teste com valor real assere sobre o conteúdo do resultado, inclui o caso de erro, e — idealmente — você deveria conseguir confirmar que ele realmente detecta falhas quebrando o código de propósito e vendo o teste ficar vermelho.

```python # Fraco: passa mesmo se a lógica estiver errada def test_calcular_desconto(): resultado = calcular_desconto(pedido) assert resultado is not None

# Forte: assere o valor e cobre o caminho de erro def test_desconto_progressivo_acima_de_mil(): pedido = Pedido(subtotal=1200) assert calcular_desconto(pedido) == 120 # 10% acima de 1000

def test_desconto_rejeita_subtotal_negativo(): with pytest.raises(ValueError): calcular_desconto(Pedido(subtotal=-50)) ```

Igualmente importante é cobrir os casos hostis, não só o caminho feliz. Entrada nula, string vazia, unicode inesperado, valores de fronteira (zero, o máximo, o mínimo, um a mais que o limite): é aí que os bugs se escondem. Uma suíte que só testa o cenário ideal está testando a parte do código que provavelmente já funciona.

Integração: o meio que costuma ser esquecido#

A camada do meio é a mais negligenciada, e a que mais dá dor de cabeça em produção. Testes unitários, por definição, isolam a unidade e substituem suas dependências por dublês. Isso é rápido e preciso, mas cria um ponto cego enorme: eles não verificam se a unidade realmente conversa direito com as coisas de verdade. O código pode estar perfeito e o SQL que ele gera pode estar errado; a lógica pode estar impecável e o formato que a API externa espera pode ter mudado.

Testes de integração fecham esse buraco. Eles exercitam a unidade contra uma versão real ou realista de suas dependências: o código contra um banco de dados de verdade (frequentemente em um contêiner efêmero), o cliente HTTP contra um servidor de teste, o consumidor de fila contra um broker real. É onde você descobre que a coluna era varchar(50) e o nome tinha 60 caracteres, que a transação não estava sendo commitada, que o índice não cobria a consulta.

A tentação de mockar o banco de dados em testes unitários e chamar isso de suficiente é forte e enganosa. Um mock do banco testa apenas que seu código faz o que você acha que o banco faz — ele reflete suas suposições, não a realidade. Bugs de integração vivem justamente na diferença entre suposição e realidade. Por isso, todo caminho que cruza uma fronteira importante — persistência, mensageria, chamada externa — merece pelo menos um teste de integração que atravesse essa fronteira de verdade.

End-to-end: poucos, críticos e estáveis#

No topo, os testes end-to-end simulam o usuário real: abrem o navegador, clicam, preenchem, esperam a resposta. Eles são os únicos que provam que o sistema inteiro, montado como em produção, entrega o resultado esperado. Nenhuma quantidade de testes unitários substitui a certeza de que o fluxo de checkout completo funciona da primeira tela ao e-mail de confirmação.

O preço dessa abrangência é alto: são lentos, caros de manter e propensos a flakiness — falhas intermitentes causadas por timing, rede, estado residual. A disciplina, portanto, é reservá-los para os poucos fluxos que, se quebrarem, causam prejuízo direto: login, checkout, o caminho principal do produto. Testar cada variação de formulário via end-to-end é desperdício; essas variações pertencem às camadas de baixo, que são mais rápidas e mais precisas.

Um teste end-to-end frágil é ativamente prejudicial, porque ensina a equipe a ignorar o vermelho. Quando "às vezes falha, roda de novo" vira hábito, a suíte perde toda autoridade — e no dia em que ela apontar um bug real, ninguém vai acreditar. Estabilidade não é um luxo no topo da pirâmide; é pré-requisito para que a camada tenha qualquer valor.

Cobertura mede o quê, exatamente#

Nenhuma conversa sobre testes escapa da métrica de cobertura, e poucas métricas são tão mal interpretadas. A cobertura de código mede a porcentagem de linhas (ou ramos) que foram executadas durante a suíte de testes. É uma informação útil — mas ela mede execução, não verificação. Uma linha coberta é uma linha que rodou durante um teste; não é uma linha cujo comportamento foi comprovado correto.

A distinção é a fonte de um dos maiores enganos em qualidade de software. É perfeitamente possível ter 100% de cobertura e nenhuma garantia real, se os testes executam todo o código mas não fazem asserções significativas sobre o resultado. O teste do "verde mentiroso" que só verifica resultado != null cobre a linha e não prova nada. Por isso a cobertura é boa para identificar o que não está testado — uma linha com zero de cobertura é definitivamente um ponto cego — mas é enganosa como prova do que está bem testado.

O uso saudável, portanto, é ler a cobertura como um mapa de buracos, não como um placar a maximizar. Perseguir um número alvo (95%, 100%) por si só cria incentivos perversos: gente escrevendo testes triviais para código trivial só para subir a métrica, enquanto a lógica realmente arriscada — cheia de ramos e casos de borda — recebe um único teste de caminho feliz. Melhor ter 70% de cobertura concentrada onde o risco vive, com asserções fortes, do que 100% de testes que passam por tudo sem verificar nada. A pergunta certa nunca é "qual a nossa cobertura?", e sim "as partes que doeriam se quebrassem estão de fato protegidas por testes que detectariam a quebra?".

Testar antes, testar depois, e a regra de nunca silenciar#

Escrever o teste antes do código — a prática de TDD — não é obrigatório para ter uma boa suíte, mas oferece um benefício que vai além da cobertura: ele força você a projetar a unidade a partir de como ela será usada, não de como será implementada. Um código difícil de testar é quase sempre um código mal desenhado — com dependências rígidas, responsabilidades misturadas, efeitos colaterais escondidos. O teste, escrito primeiro, expõe esses problemas de design cedo, quando ainda são baratos de corrigir. Nesse sentido, TDD é tanto uma ferramenta de arquitetura quanto de verificação.

Independentemente de quando você escreve os testes, existe uma regra inegociável: nunca silencie um teste para o CI ficar verde. Comentar um assert, marcar um teste como skip "só por enquanto", baixar o limiar de cobertura — todas essas manobras trocam um problema visível por um problema invisível. O teste vermelho está fazendo o trabalho dele: avisando que algo mudou. A resposta correta é entender o que mudou e consertar o código, ou atualizar o teste conscientemente se o comportamento esperado realmente mudou. Um teste desativado é um alarme desligado; o incêndio que ele detectaria continua possível.

A suíte como parte viva do sistema#

Uma boa suíte de testes não é escrita uma vez e esquecida. Ela evolui com o código, é lida e revisada com o mesmo cuidado, e é ela própria sujeita à disciplina de qualidade que aplicamos ao resto. Testes ilegíveis, com nomes vagos e asserções obscuras, envelhecem tão mal quanto código de produção ruim — e boa parte do valor de discutir os testes acontece no processo de code review, onde a equipe verifica não só se o código está certo, mas se os testes realmente provam o que dizem provar.

No fim, a pirâmide de testes é um argumento sobre economia de confiança: gaste o grosso do seu orçamento de testes onde o retorno é maior — testes rápidos, precisos e numerosos na base — e reserve as camadas caras para o que só elas conseguem provar. Uma suíte assim não é um obstáculo à velocidade; é o que torna a velocidade sustentável, porque permite mudar o sistema todo dia sem que cada mudança seja uma aposta.

Leituras relacionadas

Nenhum comentário ainda

Seja o primeiro a comentar.

Deixe seu comentário

Entre com sua conta Canverly para comentar. Você pode usar a mesma conta em qualquer site da rede.

Entrar com Canverly