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

Testes E2E: o que são, quando usar e quantos ter

Testes E2E simulam o usuário do início ao fim. Veja quando o custo compensa, quantos ter por fluxo crítico e como evitar testes flaky sem usar retry.

Testes E2E: o que são, quando usar e quantos ter
COVER · Dicas

Um teste E2E (end-to-end, ou ponta a ponta) é um teste automatizado que percorre um fluxo do sistema do jeito que o usuário percorre: abre o navegador, clica, preenche, e passa de verdade pelo frontend, pela API e pelo banco. No checkout de uma loja, o E2E faz login, adiciona um produto, confirma o pedido e verifica que a tela de confirmação apareceu. É o teste mais parecido com produção que existe, e também o mais caro.

O pipeline quebrou às 14h de uma sexta. O culpado: um teste E2E que estava flaky havia três semanas, nunca tinha falhado em desenvolvimento e escolheu justo aquela sexta para decidir que o toast de confirmação demorou mais que o waitForTimeout fixo do teste. 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.

Este post cobre o que E2E testa que nenhuma outra camada testa, quando usar, quantos ter e como escrever testes que não ficam flaky, com exemplos em Playwright que eu rodei.

Onde o E2E fica em relação aos outros testes

A pirâmide de testes resume a economia: muitos testes baratos embaixo, poucos caros em cima. Unitário roda em milissegundos e aponta a função quebrada; integração exercita banco, cache e HTTP de verdade, mas sem interface; E2E sobe tudo, com browser, servidor, banco e às vezes e-mail e webhooks. A diferença entre os dois de baixo está detalhada em teste unitário vs teste de integração.

O problema não é fazer E2E. É fazer E2E como se fosse integração: muitos, cobrindo casos que camadas mais baratas 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 vários sistemas com estado. Um teste de integração valida que a API de checkout responde 200. Um E2E valida que o usuário clicou em "Comprar", preencheu o cartão, viu a confirmação na tela e o pedido ficou gravado. Isso inclui sessão, cookies, redirecionamentos e estado do DOM, coisas que testes de integração não exercitam.

Regressões de UI sem representação no backend. Um componente de formulário que passa a mandar null em vez de string vazia por causa de um upgrade de biblioteca não aparece nos testes de serviço. Aparece no E2E.

Contratos entre frontend e backend que ninguém percebeu que mudaram. O campo virou user_id em vez de userId, e os dois lados têm testes passando. O E2E pega, porque exercita os dois juntos. Mas aqui vale um aviso: se esse é o seu problema recorrente, um teste de contrato pega a mesma quebra muito mais cedo e mais barato, sem browser.

Os três casos têm em comum: o bug só existe na interação entre as partes.

Quando usar testes E2E

Compensa quando o risco de regressão é alto e testar de outra forma é difícil:

  • Fluxos de pagamento e checkout.
  • Autenticação, especialmente com OAuth e redirecionamentos.
  • Onboarding com vários passos e estado persistido entre telas.
  • Funcionalidades que já quebraram em produção e que você quer garantir que não regridam.

Não compensa para CRUD simples com validações já cobertas por unitário, listagens paginadas sem lógica, páginas estáticas, nem para validação de campo. "E-mail inválido mostra mensagem de erro" é um teste unitário da função de validação, não uma sessão de browser de vários segundos. Contrato de endpoint isolado também não: é o terreno de testes automatizados de API.

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

Quantos testes E2E ter

Menos do que você está inclinado a escrever. Não existe proporção universal entre unitário, integração e E2E; percentuais fixos como 70/20/10 são convenção, não regra. O que vale em qualquer formato de suíte é que os E2E ficam poucos e reservados aos fluxos críticos.

O erro mais comum é usar E2E para cobrir variantes. O fluxo de cadastro tem validações: e-mail inválido, senha fraca, usuário já existente, CPF mal formatado. Alguém escreve um E2E para cada. Agora são oito testes de dezenas de segundos, que dependem de estado no banco, falham às vezes por timing no formulário e cobrem casos que um unitário cobre em milissegundos.

A heurística que funciona para mim:

  • 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.
  • O caminho feliz de cada fluxo crítico, mais os cenários de erro com histórico de regressão.
  • Zero E2E para lógica de validação e para contratos de API isolados.

Na minha experiência, isso dá poucas dezenas de testes num produto médio. Se a contagem de E2E cresce sem parar, ou a suíte passa de uns vinte minutos, provavelmente há lógica que deveria estar coberta embaixo.

Testes flaky: por que retry não resolve

Flaky é o teste que às vezes passa e às vezes falha sem que o código tenha mudado. É endêmico em E2E, e a resposta padrão ("adiciona retry") esconde o problema em vez de resolver.

Para mostrar, montei uma página local com um botão "Confirmar pedido" cujo toast aparece entre 0,5 e 2 segundos depois do clique, como uma API real sob carga. Primeiro, a versão que aposta num tempo fixo:

test('confirma o pedido (flaky)', async ({ page }) => {
  await new LoginPage(page).login(usuario);
  await page.getByRole('button', { name: 'Confirmar pedido' }).click();
  await page.waitForTimeout(1000);
  expect(await page.getByRole('status').textContent()).toBe('Pedido confirmado');
});

A documentação do Playwright é direta sobre waitForTimeout: nunca espere por tempo em produção, testes que esperam por tempo são inerentemente flaky; use ações de locator e asserções web-first, que esperam sozinhas. E page.waitForSelector, que muitos tutoriais ainda recomendam como correção, também está marcado como desencorajado em favor das mesmas asserções. A versão correta:

test('confirma o pedido', async ({ page }) => {
  await new LoginPage(page).login(usuario);

  await page.getByRole('button', { name: 'Confirmar pedido' }).click();

  // web-first assertion: tenta de novo até o texto aparecer (padrão: 5 s)
  await expect(page.getByRole('status')).toHaveText('Pedido confirmado');
});

Rodei as duas com @playwright/test 1.64 na imagem oficial mcr.microsoft.com/playwright, dez repetições cada (--repeat-each=10): a versão com asserção web-first passou 10 de 10; a versão com waitForTimeout falhou 5 de 10. Com --retries=2, a versão ruim virou "6 flaky, 1 failed, 3 passed": o retry escondeu a maior parte das falhas e mesmo assim não salvou todas.

Retry no CI não é proibido; a própria configuração sugerida pelo Playwright usa retries no CI. A diferença é o que você faz com o resultado. O relatório separa testes "flaky" (falharam e passaram no retry) dos "passed"; trate cada flaky como bug aberto, não como verde. Para diagnosticar, ligue trace: 'retain-on-failure' (ou 'on-first-retry') e abra o arquivo com npx playwright show-trace: você vê cada ação, o DOM e a rede no momento da falha.

As outras causas comuns de flakiness:

Race conditions em operações assíncronas. O teste clica em "Salvar" e verifica o resultado na hora, antes da resposta da API. A solução é a mesma: assertar o estado esperado com expect e deixar o Playwright esperar.

Dados compartilhados entre testes. Dois testes que leem e escrevem o mesmo registro vão se pisar quando rodarem em paralelo. Cada teste cria os próprios dados.

Dependência de serviços externos. E2E que chama Stripe ou um provedor de e-mail de verdade importa a instabilidade deles para o seu pipeline. Use o sandbox do provedor ou intercepte a chamada com page.route.

A regra que adotei: se um teste ficou flaky mais de duas vezes em duas semanas, ele é consertado ou apagado. Teste em que você não confia é pior do que nenhum, porque vai te enganar na hora errada.

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

Escrever um E2E é assumir um compromisso de manutenção. A UI muda: o seletor que era #submit-btn virou outro, o login ganhou um passo de 2FA, o checkout foi redesenhado. Cada mudança quebra algum E2E, não porque o produto está errado, mas porque o teste estava acoplado à implementação.

O antídoto é escrever pelo comportamento visível. getByRole('button', { name: 'Confirmar pedido' }) sobrevive a refactor de CSS e de ID; #btn-x93 não. O usuário não sabe o ID do botão, sabe o que está escrito nele. Na página do exemplo, o botão tem justamente um ID sem sentido, e o teste nem toma conhecimento.

A outra prática é o Page Object Model: em vez de repetir os passos de login em dez testes, uma classe concentra os locators e as ações. Quando a tela muda, você muda em um lugar:

export class LoginPage {
  constructor(page) {
    this.page = page;
    this.email = page.getByLabel('E-mail');
    this.senha = page.getByLabel('Senha');
    this.entrar = page.getByRole('button', { name: 'Entrar' });
  }

  async login(user) {
    await this.page.goto('/');
    await this.email.fill(user.email);
    await this.senha.fill(user.senha);
    await this.entrar.click();
    await expect(this.page.getByRole('heading', { name: 'Carrinho' })).toBeVisible();
  }
}

Repare que o próprio page object termina com uma asserção web-first: o login só "acabou" quando a próxima tela está visível, o que elimina mais uma fonte de race condition.

Perguntas frequentes

Testes E2E devem rodar em cada PR ou só no merge?

Depende do tempo da suíte. Se a suíte completa roda em poucos minutos, faz sentido em cada PR. Se é bem mais lenta, muitos times rodam um subconjunto dos fluxos críticos no PR e a suíte inteira antes do merge para a branch principal. O que não funciona é rodar tudo a cada commit de uma branch longa: dezenas de execuções por dia em código que ainda está mudando.

Cypress ou Playwright em 2026?

Para projeto novo, eu começo com Playwright: vários browsers (Chromium, Firefox e WebKit) no mesmo runner, isolamento por contexto, asserções web-first e o trace viewer. Os downloads no npm apontam na mesma direção: na semana de 1 a 7 de novembro de 2026, @playwright/test teve cerca de 115,7 milhões de downloads contra 7,7 milhões do cypress; em janeiro de 2024 a ordem era inversa (2,4 milhões contra 4,5 milhões). Download conta instalação de CI, não pessoas, então é sinal de tendência, não placar. Se você já tem uma suíte Cypress estável, não há motivo para migrar só por isso.

Vale mockar o backend nos testes E2E?

Parcialmente. Mockar o backend inteiro transforma o E2E num teste de UI caro: você verifica que o frontend renderiza um dado que você mesmo inventou. Faz sentido interceptar serviços de terceiros (pagamento, e-mail, SMS) e respostas de erro difíceis de provocar. A diferença entre os tipos de dublê está em mock, stub, fake e spy.

Como evitar repetir o login em cada teste E2E?

Com storageState do Playwright: um projeto de setup faz login uma vez, salva cookies e local storage num arquivo e os outros projetos o carregam. A documentação de autenticação recomenda guardar esse arquivo em playwright/.auth e colocar a pasta no .gitignore, porque ele contém cookies que permitem se passar pela conta de teste.

E2E é o topo porque é caro, não porque é o melhor

E2E fica no topo da pirâmide por custo, não por mérito. Ele compensa nos fluxos em que o risco de regressão é alto e a interação entre as partes é exatamente o que você precisa testar: login, checkout, onboarding. Para o resto, unitário, integração e contrato fazem o trabalho melhor e mais barato. Escreva os poucos que sobrarem com locators por papel e texto, asserções web-first e zero waitForTimeout, e trate todo teste marcado como flaky como bug. Quando um teste falha só no CI, comparar o log do run vermelho com o de um run verde no Comparador de Texto costuma mostrar em segundos a linha que divergiu.

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