Todos os artigos
96 artigos · atualizado semanalmente Veja nossas Ferramentas
Todos os artigos
Dicas

Testes end-to-end: quando o custo compensa

E2E testa o que as outras camadas não conseguem — mas é caro, lento e propenso a flakiness. Saiba quando usar, quanto ter, e por que retry não resolve.

Testes end-to-end: quando o custo compensa
COVER · Dicas

O pipeline quebrou às 14h de uma sexta. O culpado: um teste E2E que flaky há três semanas, nunca falhou em desenvolvimento, e escolheu essa sexta específica para decidir que o botão "Submit" demorou 3ms a mais que o timeout hardcoded. O deploy ficou bloqueado por quarenta minutos enquanto alguém rodava localmente, confirmava que estava tudo certo, e fazia o rerun no CI.

Esse é o custo real de E2E mal calibrado. Não é só o tempo de execução — é o custo cognitivo de desconfiar dos próprios testes.


Por que E2E é diferente dos outros dois

Se você já leu o post sobre integração versus unitário, já sabe a lógica da pirâmide: muitos testes unitários, menos de integração, pouquíssimos E2E. A pirâmide não é uma sugestão estética — ela reflete o custo real de cada camada.

Testes unitários rodam em milissegundos, não têm dependências externas, e quando falham você sabe exatamente onde está o problema. Testes de integração já têm dependências reais — banco, cache, serviços internos — mas ainda são controláveis. Testes E2E sobem tudo: browser headless, servidor, banco, serviços externos, às vezes até emails e webhooks. É o ambiente mais próximo do real, e é exatamente isso que os torna caros e quebradiços.

O problema não é fazer E2E. O problema é fazer E2E como se fosse integração — muitos, cobrindo casos que outras camadas cobrem melhor.

O que E2E cobre que as outras camadas não cobrem

Antes de discutir quantidade, vale entender o que justifica o custo.

Fluxos que atravessam múltiplos sistemas com estado. Um teste de integração pode validar que a sua API de checkout retorna 200. Um teste E2E valida que o usuário clicou em "Comprar", preencheu o cartão, recebeu a confirmação na tela, e o email de confirmação chegou na fila de envio. Isso inclui sessão, cookies, redirecionamentos, estado do DOM — coisas que testes de integração não exercitam.

Regressões de UI que não têm representação no backend. Um componente de formulário que manda null em vez de undefined por causa de uma mudança de lib não vai aparecer nos seus testes de serviço. Vai aparecer num E2E.

Contratos entre frontend e backend que nenhuma das equipes percebeu que mudou. O campo virou user_id em vez de userId. Os dois lados têm testes passando. O E2E falha — porque é o único que exercita os dois juntos de verdade.

Esses três casos têm em comum: o bug só existe na interação. Não tem como testar interação sem testar de ponta a ponta.

Quantos testes E2E faz sentido ter

A resposta honesta: menos do que você está inclinado a escrever.

O erro mais comum é usar E2E para cobrir variantes. Você tem um fluxo de cadastro. Ele tem validações: email inválido, senha fraca, usuário já existente, CPF mal formatado. Você escreve um E2E para cada. Agora você tem oito testes que levam quarenta segundos cada, que dependem de estado no banco, que às vezes falham por race condition no formulário, e que cobrem casos que um teste unitário da função de validação cobre em 3ms.

A heurística que funciona na prática:

  • Um E2E por fluxo crítico de negócio. Não por variante, não por edge case — por fluxo. Login, checkout, onboarding, cancelamento de assinatura. O que, se quebrar, para o produto.
  • Zero E2E para lógica de validação. Isso é unitário.
  • Zero E2E para contratos de API isolados. Isso é integração.
  • E2E para o caminho feliz de cada fluxo crítico, mais dois ou três cenários de erro que têm histórico de regressão.

Em projetos médios, isso costuma dar entre 10 e 30 testes E2E. Em projetos grandes, talvez 50-80 se a superfície for realmente grande. Quando você passa de 100 testes E2E, provavelmente está cobrindo coisas que outras camadas deveriam cobrir.

Flakiness: o problema que ninguém fala com honestidade

Flaky test é um teste que falha de vez em quando sem que nada tenha mudado. É endêmico em E2E. E a resposta padrão da maioria dos times — "adiciona um retry" — é pior do que o problema.

Retry esconde o problema. O teste continua quebrando, você continua ignorando, e quando ele falhar três vezes seguidas num deploy crítico você vai gastar horas debugando algo que poderia ter sido resolvido semanas atrás.

As causas mais comuns de flakiness em E2E:

Timeouts arbitrários. await page.waitForTimeout(2000) é o code smell clássico. Você está apostando que dois segundos são suficientes. Em CI sob carga, não são. A correção é esperar pelo elemento ou pelo estado, não por tempo: await page.waitForSelector('[data-testid="confirm-button"]').

Race conditions em operações assíncronas. O teste clica em "Salvar" e imediatamente verifica se o toast aparece. O toast depende de uma resposta de API que ainda não voltou. A solução é esperar pela ação, não assumir velocidade.

Dados compartilhados entre testes. Dois testes E2E que leem e escrevem no mesmo registro do banco vão se pisar eventualmente. Cada teste deve criar os próprios dados ou usar isolamento de banco por teste.

Dependência de serviços externos. E2E que faz chamada real pra Stripe, SendGrid, ou qualquer serviço de terceiro está importando a instabilidade daquele serviço pro seu pipeline. Mock ou sandbox — nunca produção em testes automatizados.

A regra que adotei: se um teste ficou flaky mais de duas vezes em duas semanas, ele é consertado ou deletado. Um teste que você não confia é pior do que nenhum teste — porque você passou o tempo escrevendo e mantendo algo que vai te enganar na hora errada.

O custo de manutenção que ninguém orça

Quando você decide escrever um E2E, você está assumindo um compromisso de manutenção. O custo não é a hora de escrever — é o custo acumulado de manter ao longo do tempo.

A UI muda. O seletor que era #submit-btn virou [data-testid="submit"]. O fluxo de login ganhou um passo de 2FA opcional. A tela de checkout foi redesenhada. Cada uma dessas mudanças quebra algum E2E — não porque o produto está errado, mas porque o teste estava acoplado a implementação, não a comportamento.

O antídoto é escrever E2E pelo comportamento do usuário, não pela estrutura do DOM. getByRole('button', { name: 'Confirmar pedido' }) é mais resiliente que #confirm-order-btn. O usuário não sabe o ID do botão — sabe o que ele diz.

A outra prática que reduz custo de manutenção: Page Object Model (POM). Em vez de repetir await page.fill('#email', user.email) em dez testes, você tem uma classe LoginPage com método login(user). Quando o seletor muda, você muda em um lugar.

Quando E2E compensa de verdade

Compensa quando o risco de regressão é alto e a dificuldade de testar de outra forma também é.

Fluxos de pagamento. Fluxos de autenticação com OAuth. Onboarding com múltiplos passos e estado persistido. Integrações críticas com serviços externos onde um mock pode dar falsa segurança. Funcionalidades que já causaram bug em produção e que você quer garantir que não regridam.

Não compensa para: CRUD simples com validações cobertas em unitários, listagens paginadas sem lógica complexa, páginas estáticas, configurações que têm teste de snapshot.

A pergunta que sempre me faço antes de escrever um E2E: "se esse teste não existisse, como eu saberia que essa funcionalidade quebrou?" Se a resposta for "via outro teste mais rápido e barato", o E2E não precisa existir.


Perguntas frequentes

Testes E2E deveriam rodar em cada PR ou só em merge?

Depende do tempo de execução. Se a suite E2E completa roda em menos de cinco minutos, faz sentido em cada PR. Se passa de dez, a maioria dos times move para antes do merge (branch protection no merge para main) ou roda em paralelo sem bloquear o PR. O que não funciona: rodar E2E em cada commit de uma feature branch longa — você vai ter dezenas de runs por dia em código que muda constantemente.

Cypress ou Playwright em 2026?

Playwright ganhou. Não é opinião — é o que os dados de adoção mostram. Suporte nativo a múltiplos browsers, melhor isolamento de contexto, API mais consistente, e o modo de trace debugging é genuinamente bom. Cypress ainda funciona bem para quem já tem a suite escrita, mas para novos projetos a escolha padrão é Playwright.

Vale mockar o backend nos testes E2E?

Parcialmente. Mockar completamente o backend transforma o E2E num teste de UI caro — você está testando que o frontend renderiza certo dado que você mesmo inventou. O valor do E2E é justamente exercitar a integração real. O que faz sentido mockar: serviços externos (Stripe, email, SMS), rate limits, e respostas de erro que são difíceis de reproduzir organicamente.

Como lidar com autenticação em E2E sem repetir login em cada teste?

Playwright tem suporte a storageState — você autentica uma vez, salva o estado do browser (cookies + localStorage), e reutiliza em outros testes. Isso elimina o overhead de autenticação de cada teste sem comprometer o isolamento. O arquivo de estado é gerado em setup global e não deve ser commitado no repositório.


O que levo disso

E2E não é o topo da pirâmide porque é o melhor — é o topo porque é o mais caro. O custo compensa para os fluxos em que o risco de regressão é alto e a interação entre camadas é o que você precisa testar. Para o resto, unitário e integração fazem o trabalho melhor e mais barato.

A métrica que uso: se a suite E2E está levando mais de vinte minutos, ela cresceu demais. Hora de auditar o que está cobrindo e mover o que pode ser coberto por integração ou unitário.

Antes de escrever o próximo E2E, vale a pergunta: qual a camada mais barata que pode detectar essa regressão?


Nota: o conteúdo editorial acabou aqui. O que vem abaixo é uma indicação de ferramenta relacionada ao tema do post.


Ferramenta relacionada

Quando estou revisando outputs de diferentes rodadas de teste — comparando logs de CI entre um run que passou e um que falhou, ou verificando se um snapshot de resposta de API mudou entre versões — uso o Comparador de Texto para ver exatamente o que divergiu. Especialmente útil para diagnosticar flakiness que produz outputs ligeiramente diferentes a cada run.

RD
Autor
Rafael Duarte
Desenvolvedor backend com passagem por fintech e SaaS B2B — trabalhou em times que escalaram APIs de zero a milhões de requisições. Carrega cicatrizes de produção suficientes para ter opiniões fortes sobre ferramentas, padrões e decisões de arquitetura. Não é acadêmico: leu a RFC do UUID quando precisou escolher entre v4 e v7 para uma tabela de alta escrita.
Ver perfil