End-to-End Testing

Testes End-to-End

Testes end-to-end operam o sistema real da mesma forma que um usuário faria. Eles são lentos, poucos e insubstituíveis: os únicos testes capazes de provar que uma pessoa consegue realmente concluir uma tarefa.

intermediate15 min readUpdated 16 de set. de 2026
checkout.spec.ts
ts
// tests/checkout.spec.ts
import { test, expect } from "@playwright/test";

test("a signed-in user can buy a plan", async ({ page }) => {
  await page.goto("/pricing");

  await page.getByRole("button", { name: "Choose Pro" }).click();
  await page.getByLabel("Card number").fill("4242 4242 4242 4242");
  await page.getByRole("button", { name: "Pay" }).click();

  await expect(
    page.getByRole("heading", { name: "You're on Pro" }),
  ).toBeVisible();
});
Escopo
O sistema completo
Velocidade
Segundos por teste
Quantidade
Poucos e de alto valor
Ferramentas comuns
Playwright, Cypress
Executa em
Um browser real
Responsável por
Jornadas críticas do usuário

Por que importa

O que os testes e2e proporcionam

Confiança em todo o fluxo

Um teste e2e atravessa a UI, a API, o banco de dados e retorna, sendo assim o único teste que pode provar que um usuário consegue realmente concluir uma tarefa.

Uma rede de segurança contra regressões

Alguns poucos testes de jornada capturam quebras de integração que os testes unitários ignoram: um campo renomeado, um redirecionamento quebrado ou uma migration ausente.

Documentação executável

Uma especificação legível descreve o produto na linguagem do usuário e, ao contrário de uma página de wiki, ela quebra o build quando se torna obsoleta.

O panorama completo

As três camadas de um teste e2e

Prepare o ambiente, execute o caminho do usuário e valide o que uma pessoa veria.

A stack

Arrange

Um banco de dados, API e frontend reais, populados em um estado conhecido. Se o ambiente não for reproduzível, nenhuma asserção é confiável.

A jornada

Act

Opere a interface da mesma forma que uma pessoa faria, através de roles, labels e texto visível, em vez de detalhes de implementação.

O resultado

Assert

Verifique o que o usuário vê e, quando for relevante, confirme o efeito colateral através da API ou do banco de dados.

HTML5 de uma olhada

O toolkit de e2e

Navegar

page.goto carrega uma rota e o teste começa a partir de uma URL real.

Localizar

getByRole, getByLabel e getByText encontram elementos como um usuário faria.

Agir

click, fill, type e select operam a interface.

Validar

expect(locator).toBeVisible() tenta novamente até que o resultado seja verdadeiro.

Autenticar

Reutilize um storageState salvo em vez de fazer login em cada teste.

Observar

Aguarde respostas, bloqueie terceiros e valide chamadas de API.

Fluxo

A jornada por trás de cada teste aprovado

Um bom teste e2e prepara o mundo, realiza uma jornada e deixa o ambiente como o encontrou.

  1. 1

    Popular dados via API

    Crie o usuário, o plano e quaisquer registros que a jornada precise com chamadas de API, não clicando em telas de configuração.

  2. 2

    Abrir o app

    Navegue para a rota de entrada com uma sessão autenticada já ativa, para que o teste comece onde o usuário começaria.

  3. 3

    Realizar a ação

    Opere a interface através de locators de role e label, um passo de usuário por vez, sem pausas de tempo arbitrário.

  4. 4

    Validar o resultado visível

    Verifique a coisa que o usuário veio ver. Quando o resultado for um efeito colateral, confirme-o também via API.

  5. 5

    Capturar evidências em falhas

    Mantenha traces, screenshots e vídeos para que um build vermelho no CI possa ser diagnosticado sem a necessidade de reprodução local.

  6. 6

    Resetar o estado

    Delete ou expire o que o teste criou, ou execute cada teste contra dados isolados para que a ordem nunca importe.

Uma breve historia

A evolução dos testes de browser

  1. 2006

    Chegada do Selenium

    A automação de browser torna-se possível, e os testes end-to-end tornam-se uma prática real com uma curva de aprendizado íngreme.

    06
  2. 2015

    Fundação do Cypress

    Um runner amigável ao desenvolvedor coloca o browser no mesmo processo e torna o debugging muito mais fácil.

    15
  3. 2020

    Lançamento do Playwright

    Uma biblioteca cross-browser com auto-waiting redefine as expectativas sobre o quão estáveis os testes de browser podem ser.

    20
  4. 2021

    Playwright Test

    Um runner oficial adiciona fixtures, paralelismo, tracing e sharding à biblioteca.

    21
  5. Hoje

    Um portão de qualidade focado

    As equipes mantêm suítes e2e pequenas e de alto valor, executando-as em pull requests e antes do release.

    Hoje

O guia completo

Testes End-to-End: Tudo que voce precisa saber

O que testes end-to-end realmente significam

Testes end-to-end (e2e) operam o sistema real do ponto de vista do usuário. Em vez de chamar uma função ou uma rota isoladamente, um teste e2e abre a aplicação, executa as mesmas etapas que uma pessoa faria e verifica se o resultado que o usuário veria realmente aconteceu.

Essa definição traz duas consequências. Primeiro, o teste atravessa todas as fronteiras do sistema: browser, código frontend, HTTP, API, banco de dados e vice-versa. Segundo, as asserções são sobre comportamento, não sobre implementação. Você valida que uma confirmação está visível, e não que uma função específica foi chamada.

Como toda a stack está envolvida, os testes e2e são a coisa mais próxima de uma garantia de que o usuário consegue concluir uma tarefa. Eles também são os testes mais lentos e caros que você escreverá, e é exatamente por isso que você deve escrever poucos deles e escolhê-los cuidadosamente.

Onde o e2e se encaixa na pirâmide de testes

A pirâmide de testes é uma regra prática sobre a quantidade de cada tipo de teste que você deve escrever. A base é larga: muitos testes unitários rápidos. O meio é mais estreito: testes de integração entre alguns componentes. O topo é pequeno: algumas poucas jornadas de ponta a ponta (end-to-end).

  • Testes unitários levam milissegundos, rodam sem infraestrutura e validam a lógica pura e casos de borda (edge cases).
  • Testes de integração levam segundos, exercitam uma rota ou um módulo com colaboradores reais e capturam bugs de conexão (wiring bugs).
  • Testes end-to-end levam dezenas de segundos, rodam contra uma stack implantada e provam que um usuário consegue completar uma tarefa crítica.

O formato importa porque o custo aumenta conforme você sobe e o feedback torna-se mais lento. Um bug que um teste unitário pode encontrar deve ser encontrado por um teste unitário. Reserve o e2e para as jornadas onde um caminho quebrado significa um produto quebrado: cadastro, login, checkout, publicação, convite. Se um teste não precisa de um navegador para ser significativo, ele provavelmente não deveria usar um.

Uma verificação de sanidade útil: para cada teste e2e, pergunte qual teste unitário ou de integração teria capturado o mesmo bug. Se a resposta for “um que é fácil de escrever”, o teste e2e está na camada errada.

Escolhendo as jornadas que valem a pena automatizar

Você não pode automatizar todos os caminhos, portanto, escolha aqueles onde a falha custa mais caro e onde é mais provável que um bug escape pelas camadas inferiores.

Pontue uma jornada candidata em dois eixos: impacto no negócio e risco de integração. Alto impacto somado a alto risco é onde o e2e prova seu valor.

  • Alto impacto, alto risco — cadastro, login, checkout, redefinição de senha. Automatize estes primeiro.
  • Alto impacto, baixo risco — a home page de marketing. Cubra-a com um smoke test, não com uma jornada completa.
  • Baixo impacto, alto risco — uma exportação de admin usada mensalmente. Um único teste é suficiente, ou uma verificação via script.
  • Baixo impacto, baixo risco — toggles de configurações e estados cosméticos. Deixe-os para testes de unidade e de componente.

As jornadas que cruzam mais fronteiras são as mais valiosas, pois são aquelas que um teste de unidade não consegue alcançar. Um checkout toca o carrinho, precificação, pagamento, o serviço de pedidos e e-mail — exatamente a fiação que quebra silenciosamente quando uma equipe renomeia um campo.

Anote a lista e revise-a a cada trimestre. O conjunto de jornadas críticas muda conforme o produto cresce, e o fluxo importante de ontem pode se tornar o código morto de hoje.

Escolhendo uma ferramenta: Playwright ou Cypress

Duas ferramentas dominam os testes de navegador modernos, e ambas são boas.

Playwright controla Chromium, Firefox e WebKit através de uma única API. Por padrão, ele executa testes em processos de worker paralelos, suporta múltiplos projetos de navegador, possui locators com auto-waiting e vem com um trace viewer que grava a execução quadro a quadro. Seu storageState torna a reutilização de autenticação simples, e sua fixture request fornece um cliente de API no mesmo teste. É a escolha padrão comum para novas suítes.

Cypress roda no event loop do navegador, o que torna a depuração imediata: você pode inspecionar o DOM a qualquer momento e fazer “viagem no tempo” através dos comandos no runner. Sua API é amigável e suas mensagens de erro são excelentes. A contrapartida é que o suporte cross-browser e o paralelismo historicamente exigiram mais configuração.

A escolha raramente importa tanto quanto a disciplina. Uma suíte de Playwright bem escrita e uma suíte de Cypress bem escrita funcionam igualmente bem. Escolha uma, aprenda seu modelo de espera e foque sua energia em isolamento e locators.

A anatomia de um teste

Quase todo teste e2e possui as mesmas três partes, geralmente chamadas de arrange, act e assert.

test("a signed-in user can upgrade to Pro", async ({ page }) => {
  // arrange: the account and plan already exist
  // act: perform the journey
  await page.goto("/pricing");
  await page.getByRole("button", { name: "Choose Pro" }).click();

  // assert: the user sees the result
  await expect(
    page.getByRole("heading", { name: "You're on Pro" }),
  ).toBeVisible();
});

Mantenha a etapa de arrange fora da UI sempre que possível. Criar um usuário clicando em um formulário de cadastro adiciona minutos a uma suite e acopla cada teste ao fluxo de signup. Em vez disso, crie-o através da API e inicie o teste já logado.

Outro hábito que vale a pena cultivar é ter apenas uma jornada por teste. Quando um teste cobre cinco coisas e falha, você descobre que uma dessas cinco coisas quebrou. Quando ele cobre apenas uma, o nome do teste que falhou já é o diagnóstico.

Escrevendo uma jornada com boa leitura

Um teste e2e é lido com muito mais frequência do que é escrito. O leitor deve ser capaz de entender o que ele protege sem precisar abrir o app.

Nomeie o teste com base no resultado para o usuário, não na implementação. “um usuário logado pode fazer upgrade para o Pro” é melhor do que “testar clique no botão de checkout”. Use o tempo presente e o vocabulário do usuário.

Estruture o teste como uma sequência de ações do usuário, uma por linha, com linhas em branco entre as fases.

test("an admin can invite a teammate", async ({ page }) => {
  await page.goto("/team");

  await page.getByRole("button", { name: "Invite" }).click();
  await page.getByLabel("Email").fill("[email protected]");
  await page.getByRole("button", { name: "Send invite" }).click();

  await expect(page.getByText("Invitation sent")).toBeVisible();
});

Extraia jornadas repetidas para helpers, mas mantenha o teste em si legível. Um helper chamado upgradeToPro(page) é bom; um helper que esconde o teste inteiro não é. O teste ainda deve mostrar as etapas.

Por fim, comente o “porquê”, não o “quê”. O código já diz qual botão foi clicado; um comentário só é útil quando explica por que o teste existe ou por que uma etapa parece estranha.

Locators: fale com o usuário, não com o DOM

Um locator é a forma como o teste encontra um elemento. A maior melhoria que você pode fazer em uma suite de e2e é escolher locators da mesma maneira que um usuário faria.

O Playwright os ordena do mais para o menos preferido:

  1. getByRole — botões, links, cabeçalhos e inputs por role acessível e nome.
  2. getByLabel — controles de formulário por seu label.
  3. getByText — texto visível.
  4. getByPlaceholder, getByAltText, getByTitle — outros atributos visíveis.
  5. getByTestId — um test id estável quando nada voltado ao usuário for adequado.
await page.getByRole("button", { name: "Add to cart" }).click();
await page.getByLabel("Email").fill("[email protected]");
await expect(page.getByRole("heading", { name: "Your cart" })).toBeVisible();

Essa ordenação não é estética. Locators de role e label falham quando um elemento é inacessível, portanto, o teste serve também como uma verificação de acessibilidade. Eles também sobrevivem a refatorações: renomear uma classe CSS não os quebra, mas quebra .btn.btn--primary > span.

Use getByTestId deliberadamente, não por padrão. Um test id é um contrato que você mantém, e uma suite cheia deles não diz nada sobre se a interface faz sentido.

Lidando com a autenticação

Fazer login pela UI antes de cada teste é a causa mais comum de suítes de e2e lentas e instáveis. Um login envolve um formulário, uma requisição de rede e um redirecionamento, repetidos centenas de vezes.

A solução é autenticar apenas uma vez e reutilizar a sessão. O Playwright chama a sessão salva de storageState — cookies e local storage serializados em um arquivo.

// tests/auth.setup.ts
import { test as setup, expect } from "@playwright/test";

setup("authenticate", async ({ page }) => {
  await page.goto("/login");
  await page.getByLabel("Email").fill(process.env.E2E_EMAIL!);
  await page.getByLabel("Password").fill(process.env.E2E_PASSWORD!);
  await page.getByRole("button", { name: "Sign in" }).click();

  await expect(page.getByRole("heading", { name: "Dashboard" })).toBeVisible();
  await page.context().storageState({ path: "playwright/.auth/user.json" });
});

Um projeto de setup escreve o arquivo, e os projetos de browser dependem dele e o carregam através de use.storageState. Testes que precisam de um usuário diferente utilizam um arquivo de estado diferente.

// playwright.config.ts
export default defineConfig({
  projects: [
    { name: "setup", testMatch: /auth\.setup\.ts/ },
    {
      name: "chromium",
      use: { storageState: "playwright/.auth/user.json" },
      dependencies: ["setup"],
    },
  ],
});

Você ainda deve manter um teste que faça o login através da interface. O formulário de login é uma jornada do usuário por si só, e o atalho nunca deve ser a única cobertura para essa funcionalidade.

Tornando os testes determinísticos

Determinismo é a chave de tudo. Um teste que passa às vezes é pior do que um que falha sempre, porque ele ensina a equipe a ignorar o vermelho.

Três regras cobrem a maior parte disso.

Nunca use sleep. waitForTimeout(3000) ou é curto demais e instável, ou longo demais e lento. Em vez disso, aguarde pela condição: await expect(locator).toBeVisible(). Asserções focadas em web tentam novamente até passarem ou expirarem o tempo limite, que é exatamente o que você deseja.

Controle a rede. Faça mock de terceiros e dependências instáveis com page.route, para que um sandbox de pagamento lento não quebre a sua suíte de testes. Faça asserções nas requisições que seu app faz quando esse for o comportamento sob teste.

await page.route("**/api/recommendations", (route) =>
  route.fulfill({ json: { items: [] } }),
);

Congele o tempo e a aleatoriedade. Um teste que depende de “hoje” ou de um id aleatório falhará em algum momento limite. Injete um clock ou defina seeds para os valores que o teste lê.

Há também o próprio navegador: animações, autofocus e transições criam condições de corrida (races) que parecem bugs da aplicação. Desabilite as animações no ambiente de teste sempre que possível e prefira asserções sobre o estado final.

Cobertura responsiva e entre navegadores

Uma jornada que funciona no Chromium pode falhar no WebKit, e um layout que funciona em um laptop pode ser inutilizável em um celular. A cobertura de viewports e de diferentes navegadores é uma das poucas coisas que o e2e faz que nenhuma outra ferramenta consegue.

O Playwright transforma isso em um problema de configuração. Defina um projeto por navegador e reutilize os mesmos testes.

// playwright.config.ts
export default defineConfig({
  projects: [
    { name: "chromium", use: { ...devices["Desktop Chrome"] } },
    { name: "firefox", use: { ...devices["Desktop Firefox"] } },
    { name: "webkit", use: { ...devices["Desktop Safari"] } },
    { name: "mobile", use: { ...devices["iPhone 13"] } },
  ],
});

Nem todo teste precisa de todos os navegadores. Execute a suíte completa no Chromium para obter feedback rápido, e execute as jornadas críticas em todos os projetos de forma agendada ou antes do release. Isso mantém os pull requests ágeis sem abrir mão da cobertura.

Quando um bug for específico de um navegador, adicione um teste de regressão no projeto afetado. Um comentário que linka para a issue vale mais do que uma nota vaga dizendo que “o Safari é estranho”.

Gerenciando dados de teste

Os dados são onde as suítes de e2e silenciosamente se tornam instáveis. Dois testes que criam um usuário chamado [email protected] passarão individualmente, mas falharão quando executados juntos.

Faça o seed através da API. É mais rápido do que via UI, falha de forma clara quando o seed quebra e mantém o teste focado na jornada, em vez de focar na configuração.

const res = await request.post("/api/test/users", {
  data: { email: `ada+${Date.now()}@example.com` },
});
expect(res.ok()).toBeTruthy();
const user = await res.json();

Isole os dados por teste ou por worker. E-mails únicos, registros com escopo de tenant ou um namespace novo por worker funcionam bem. O que importa é que nenhum teste dependa da mesma linha do banco de dados.

Limpe o que você criar, ou torne a limpeza desnecessária definindo o escopo dos dados para uma execução de teste e deletando todo o escopo ao final. Registros remanescentes se acumulam, deixam o banco de dados lento e, eventualmente, causam falhas que não têm relação com a alteração atual.

Executando contra uma stack real

Um teste e2e precisa de um lugar para rodar: um frontend, uma API e um banco de dados, todos na mesma versão do código. A opção mais reproduzível é uma stack docker compose que inicia tudo com um único comando.

docker compose -f docker-compose.e2e.yml up -d --wait
pnpm exec playwright test
docker compose -f docker-compose.e2e.yml down -v

A flag --wait é fundamental: ela faz com que o Compose retorne apenas quando os health checks passarem, o que elimina a race condition de “conexão recusada no primeiro teste”. O -v na finalização descarta os volumes para que a próxima execução comece do zero.

Ambientes de preview levam isso adiante. Uma plataforma faz o build da branch, a implanta em uma URL temporária e a expõe para o job de teste. Isso é o mais próximo da produção que uma execução de e2e pode chegar, e permite que as equipes de produto e QA naveguem pelo mesmo build que os testes utilizam.

Aponte a suite para o ambiente usando uma variável de ambiente — BASE_URL — e nunca deixe o host hard-coded. A mesma suite deve rodar localmente, em CI e contra um preview.

Quando a jornada é uma API, não um navegador

Nem toda jornada de ponta a ponta precisa de um navegador. Um webhook, um job em segundo plano, uma CLI e um fluxo de serviço para serviço são todos “end-to-end” no sentido que importa: eles exercitam o sistema real.

A fixture request do Playwright é um cliente de API completo, portanto, uma jornada de API parece quase idêntica a uma de navegador.

test("a paid order is fulfilled end to end", async ({ request }) => {
  const order = await request.post("/api/orders", {
    data: { sku: "pro-plan", quantity: 1 },
  });
  expect(order.status()).toBe(201);

  await request.post("/api/payments/webhook", {
    data: { orderId: (await order.json()).id, status: "paid" },
  });

  const res = await request.get(`/api/orders/${(await order.json()).id}`);
  expect(await res.json()).toMatchObject({ status: "fulfilled" });
});

Esses testes são mais rápidos e muito menos instáveis (flaky) do que os testes de navegador, então use-os para os fluxos que genuinamente não precisam de uma UI. Para cobertura de API pura em um nível mais baixo, o Supertest é a ferramenta mais leve.

Executando e2e em CI

Testes E2E devem ser executados em pull requests e antes de um release, não a cada salvamento. Em CI, algumas flags fazem a diferença entre um gate útil e um que é ignorado.

  • Execute em modo headless. Não há interface gráfica; Playwright e Cypress já vêm configurados como headless por padrão em CI.
  • Faça o sharding da suite. Distribua os arquivos entre máquinas com --shard=1/3 para que o tempo total de execução permaneça constante à medida que a suite cresce.
  • Tente novamente uma vez (retry). Uma única tentativa de reexecução distingue uma falha genuína de um soluço de infraestrutura, desde que você monitore a taxa de retries em vez de permitir que eles escondam a instabilidade (flakiness).
  • Faça o upload de artefatos em caso de falha. Traces, screenshots e vídeos são a maneira de tornar um build vermelho em CI diagnosticável sem a necessidade de uma reprodução local.
- run: pnpm exec playwright test --shard=${{ matrix.shard }}/3 --retries=1
- uses: actions/upload-artifact@v4
  if: ${{ !cancelled() }}
  with:
    name: report-${{ matrix.shard }}
    path: playwright-report/

Retries são uma rede de segurança, não uma solução. Se um teste precisa de um retry para passar consistentemente, ele possui uma race condition ou um bug de isolamento real; o retry está apenas lhe dando tempo para encontrá-lo.

Depurando uma execução com falha

A primeira pergunta após uma falha é sempre a mesma: o que o navegador realmente viu? Uma ferramenta que consiga responder a isso sem a necessidade de uma reprodução local transforma uma investigação de duas horas em uma de dois minutos.

O Playwright grava um trace — screenshots, snapshots do DOM, rede e console — e o trace viewer reproduz a execução passo a passo. Mantenha os traces ativos em caso de falha, abra o relatório e navegue até o momento exato em que a asserção falhou.

pnpm exec playwright test --trace on-first-retry
pnpm exec playwright show-trace test-results/checkout/trace.zip

Localmente, execute a suíte em modo headed ou no modo UI para observar o teste e pausá-lo. O Cypress oferece a mesma ideia através de seu runner interativo, que mantém um log de comandos pelo qual você pode navegar no tempo.

pnpm exec playwright test --ui
pnpm exec playwright test --headed --debug

O hábito que torna a depuração rápida é fazer a asserção exatamente naquilo que está errado. Uma falha em expect(page).toHaveURL(/\/checkout/) informa que a navegação nunca aconteceu, o que é uma investigação completamente diferente de uma falha no título de confirmação.

Combatendo a instabilidade (flakiness)

A instabilidade (flakiness) é o “imposto” dos testes e2e, e quase sempre é causada por um destes fatores:

  • Um await ausente ou uma race condition. O teste agiu antes de a aplicação estabilizar. Corrija isso com uma web-first assertion, e não com um sleep.
  • Dados compartilhados. Dois testes alteraram a mesma linha no banco. Isole os dados por teste ou por worker.
  • Uma dependência externa. Um script ou API de terceiros estava lento. Faça o mock ou remova-o do caminho crítico.
  • Um locator frágil. O teste estava vinculado a uma marcação que mudou. Migre para um locator de role ou label.
  • Uma animação. O elemento estava presente, mas ainda não estava estável. Desative as animações no ambiente de teste.

A maneira de corrigir a instabilidade é tratar cada “flake” como um bug com uma causa, colocá-lo em quarentena e corrigir a origem do problema. Uma suíte que depende de retentativas para ficar “verde” é uma suíte em que ninguém confia, e uma suíte em que ninguém confia é pior do que não ter suíte nenhuma.

Mantendo a suíte saudável ao longo do tempo

Uma suíte de e2e tem uma tendência natural de crescer até se tornar lenta e “vermelha”. Mantê-la saudável é uma prática contínua, não uma configuração única.

Revise a suíte da mesma forma que você revisa o produto. Quando uma funcionalidade é removida, delete o teste correspondente no mesmo pull request. Um teste que não protege mais nada é puro custo.

Acompanhe as métricas que importam: tempo total de execução, taxa de aprovação e a taxa de retry. Uma taxa de retry crescente é o sinal mais precoce de que a instabilidade (flakiness) está surgindo, muito antes de a suíte se tornar visivelmente não confiável.

Assuma a responsabilidade pela suíte. Uma suíte de e2e compartilhada e sem dono tende a se degradar: falhas são apenas reiniciadas, testes são ignorados e, em um trimestre, ninguém mais confia em uma execução vermelha. Atribua um rodízio ou um pequeno grupo para mantê-la verde, e transforme a correção de um teste instável em uma tarefa prioritária, em vez de tratá-la como uma interrupção.

Mantenha a suíte rápida o suficiente para ser executada em cada pull request. Quando ela ultrapassar esse limite, utilize sharding, paralelize a execução ou mova as jornadas menos críticas para uma execução noturna. Um quality gate que leva uma hora é um quality gate que as pessoas tentam contornar.

O que não cobrir com e2e

A tentação é testar tudo através do browser porque parece ser a experiência real. Resista. O E2E é a camada mais cara, portanto, utilize-a apenas onde ela realmente for necessária.

Não cubra com e2e:

  • Lógica pura e edge cases. Formatação de datas, cálculo de preços, regras de validação — isso pertence a testes unitários que rodam em milissegundos.
  • Todos os caminhos de erro. Uma página 500 vale a pena em uma jornada; as vinte formas pelas quais a API pode falhar são testes de integração.
  • Combinações exaustivas de input. O E2E cobre o caminho representativo, não a matriz completa.
  • Comportamento de componentes. A abertura de um dropdown pertence a um teste de componente, que é mais rápido e preciso.
  • Qualquer coisa já coberta em camadas inferiores. Duplicar um teste unitário na camada de e2e adiciona custo e um novo modo de falha sem adicionar confiança.

A regra de ouro: se o bug puder ser encontrado sem um browser, encontre-o sem um browser.

Melhores práticas

  • Escreva poucos testes e2e e faça com que cada um represente uma jornada crítica.
  • Popule os dados através da API e inicie os testes já autenticado.
  • Use locators de role, label e text; trate test ids como último recurso.
  • Aguarde por condições com web-first assertions, nunca com waitForTimeout.
  • Isole os dados por teste ou por worker para que a ordem de execução nunca importe.
  • Mantenha apenas uma jornada por teste para que a falha identifique a causa.
  • Aponte a suite para BASE_URL; execute-a localmente, em CI e em previews.
  • Capture traces e vídeos em caso de falha e faça o upload como artefatos de CI.
  • Coloque testes instáveis (flakes) em quarentena e corrija-os; nunca normalize retries como solução.
  • Mantenha pelo menos um teste de login real, mesmo quando reutilizar storageState.

Erros comuns

  • Fazer login pela UI antes de cada teste.
  • Selecionar elementos por classe CSS ou posição no DOM.
  • Adicionar waitForTimeout para “corrigir” uma race condition.
  • Compartilhar um usuário ou registro seedado entre testes.
  • Executar contra um servidor local iniciado manualmente em vez de uma stack reprodutível.
  • Testar lógica de nível unitário através do navegador.
  • Hard-codar localhost:3000 para que a suite execute apenas em uma máquina.
  • Não fazer upload de artefatos e, depois, não conseguir debugar uma falha de CI.
  • Deixar um teste flaky continuar falhando e sendo reiniciado em vez de colocá-lo em quarentena.

Próximos passos

O teste E2E está no topo da pirâmide e é mais eficiente quando as camadas abaixo dele estão saudáveis. Leia sobre Supertest para testes de API rápidos que devem cobrir a maior parte da sua superfície HTTP, e Playwright para um tour mais detalhado sobre a ferramenta. Se você estiver avaliando alternativas, o Cypress cobre o outro principal runner, e o Docker explica como levantar a stack reprodutível da qual sua suíte de testes depende.

Na pratica

Quatro peças de uma suíte e2e real

Uma jornada, um atalho de login, dados populados e o job de CI que os executa.

tests/checkout.spec.ts
import { test, expect } from "@playwright/test";

test("a signed-in user can upgrade to Pro", async ({ page }) => {
  await page.goto("/pricing");

  await page.getByRole("button", { name: "Choose Pro" }).click();
  await expect(page).toHaveURL(/\/checkout/);

  await page.getByLabel("Card number").fill("4242 4242 4242 4242");
  await page.getByLabel("Expiry").fill("12/30");
  await page.getByLabel("CVC").fill("123");
  await page.getByRole("button", { name: "Pay" }).click();

  await expect(
    page.getByRole("heading", { name: "You're on Pro" }),
  ).toBeVisible();
});

Locators baseados em Role vs Seletores CSS

Locators de role e label descrevem o que o usuário vê e falham quando o elemento é inacessível. Seletores CSS quebram a cada refatoração e testam silenciosamente a marcação em vez do comportamento.

Preferir
await page
  .getByRole("button", { name: "Add to cart" })
  .click();
Evitar
await page
  .locator("div.product > button.btn.btn--primary")
  .click();

Esperar por uma condição vs sleep

Asserções web-first tentam novamente até que a condição seja atendida. Um sleep fixo é ou curto demais e instável, ou longo demais e lento, e ele esconde a race condition que deveria resolver.

Preferir
await expect(
  page.getByText("Order confirmed"),
).toBeVisible();
Evitar
await page.waitForTimeout(5000);
await expect(
  page.getByText("Order confirmed"),
).toBeVisible();

Trade-offs

O custo real dos testes end-to-end

Testes E2E são os únicos que provam que um usuário consegue concluir uma tarefa, e os mais caros de manter. Use-os com cautela.

Strengths

  • Eles testam o que os usuários fazem

    Apenas um teste e2e exercita a UI, API e banco de dados reais juntos, capturando bugs de fiação que nenhum teste unitário consegue ver.

  • Eles sobrevivem a refatorações

    Escritos com base em roles e texto visível, um teste de jornada continua passando enquanto componentes, frameworks e APIs internas mudam.

  • São uma linguagem compartilhada

    Produto, QA e engenharia podem ler a mesma especificação e concordar sobre o que significa 'estar funcionando'.

Trade-offs

  • São lentos e custosos

    Um teste de browser leva segundos e exige infraestrutura real, então uma suíte de centenas transforma cada pull request em uma pausa para o café.

  • São instáveis por natureza

    Timing, redes, animações e dados compartilhados conspiram para falhar testes que não são cuidadosamente isolados e aguardados.

  • Fornecem falhas vagas

    Um teste de jornada vermelho diz que algo em um caminho longo quebrou. Identificar o que exatamente exige traces e, frequentemente, uma reprodução local.

Perguntas frequentes

Perguntas frequentes

Keep learning

Related topics from the roadmap.

$ comecar a aprender

Pronto para aprender End-to-End Testing?

Nosso tutorial interativo te guia por End-to-End Testing passo a passo — com quizzes e codigo real que voce pode executar no navegador.