Um teste E2E pode ficar verde sem fazer uma única chamada ao serviço que você acha que está verificando. Isso acontece quando page.route() intercepta a requisição e devolve um JSON fixo. O navegador provou que a tela reage àquele JSON. Ainda não provou que a API aceita o método, os headers, o corpo ou o formato da resposta.

A solução não é abandonar mocks. É dar a cada teste uma pergunta menor. Use route.fulfill para verificar estados da interface com respostas determinísticas. Use route.fetch quando precisar partir da resposta real e alterar apenas um campo. Use HAR para reproduzir uma sequência de rede conhecida. Depois, rode um teste de integração separado contra staging, sandbox ou um contrato verificado pelo provedor.

Este artigo parte do guia de testes end-to-end com Playwright e trata de uma dúvida mais estreita: como isolar a rede sem transformar o isolamento em falsa confiança.

Diagrama compara um mock de API rápido no Playwright com uma verificação real do contrato externo.

Regra prática

  • route.fulfill testa a reação da aplicação a um caso conhecido.
  • route.fetch mantém uma chamada real e altera a resposta antes de entregá-la ao navegador.
  • routeFromHAR reproduz tráfego gravado, mas depende de correspondência de URL, método e payload.
  • Um teste separado precisa verificar o contrato real. O mock não faz esse trabalho sozinho.

O que um mock de API realmente prova?

Um mock prova o comportamento do cliente diante da resposta que você escolheu. A documentação de Mock APIs do Playwright mostra que route.fulfill pode impedir a chamada e devolver dados definidos no teste. Essa é uma propriedade útil: o fluxo não depende da disponibilidade, latência ou estado de um terceiro durante cada execução do navegador.

O que fica fora da prova é igualmente importante. O teste não descobre que o endpoint mudou de POST para PUT, que o servidor passou a exigir um header, que o schema removeu um campo ou que a autenticação expirou. O mock responde antes que essas condições tenham chance de aparecer.

Isso não é um defeito do Playwright. É o limite do nível de teste. Um teste de interface pergunta se a pessoa consegue concluir uma ação quando recebe um estado. Um teste de contrato ou integração pergunta se dois sistemas ainda concordam sobre a mensagem. O guia de testes de software ajuda a separar essas perguntas antes de escolher a ferramenta.

O falso positivo aparece quando uma equipe chama o primeiro teste de "teste da API". O nome sugere uma cobertura que o código não tem. Prefira nomes que declarem a fronteira, como checkout mostra erro quando o pagamento responde 503 e sandbox aceita o contrato de criação de pagamento.

Esse problema também aparece em testes Playwright gerados por IA: uma sequência plausível e uma asserção verde não provam que a dependência certa foi exercitada.

Quando usar route.fulfill, route.fetch ou HAR?

Escolha a forma de interceptação pelo que precisa permanecer sob controle. A documentação de Network do Playwright separa interceptar uma requisição, modificá-la, abortá-la e alterar sua resposta. A diferença é pequena no código, mas muda o que o teste consegue provar.

Técnica A chamada chega à API? Melhor pergunta Risco principal
route.fulfill Não A UI trata este estado conhecido? O fixture pode divergir do contrato real.
route.fetch + route.fulfill Sim, durante o teste A UI trata uma resposta real com uma variação controlada? O teste herda instabilidade e dados externos.
routeFromHAR Não, usa o arquivo gravado O fluxo reproduzido funciona com uma conversa de rede conhecida? O HAR fica velho ou não casa com o payload atual.
request ou APIRequestContext Sim, no teste de API O serviço aceita a requisição e entrega o schema esperado? Requer ambiente controlado e credenciais de teste.

O Playwright Mock APIs documenta os três primeiros caminhos. Para o quarto, o APIRequestContext permite chamar endpoints, preparar o ambiente e verificar o serviço sem passar pela interface. Essa divisão costuma produzir uma suíte menor e com falhas mais fáceis de localizar.

Não escolha HAR apenas porque gravar uma sessão parece rápido. Ele é bom quando o fluxo tem várias requisições estáveis e o arquivo pode ser revisado junto com o código. Para um caso de erro simples, um fixture explícito mostra melhor qual estado a pessoa está vendo e por que o teste passa.

Como criar um mock determinístico no Playwright?

O teste abaixo verifica uma tela que carrega recomendações de /api/recommendations. Ele é ilustrativo: adapte a URL, o HTML e o contrato ao seu projeto. A parte importante é registrar a rota antes de page.goto(), usar um fixture pequeno e provar que a requisição esperada realmente passou pelo interceptor.

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

test("mostra recomendações quando a API responde com dados", async ({ page }) => {
  let intercepted = false;

  await page.route(/\/api\/recommendations\?userId=test-user$/, async (route) => {
    intercepted = true;
    await route.fulfill({
      status: 200,
      contentType: "application/json",
      json: {
        items: [{ id: "book-1", title: "Testing at the Boundary" }],
      },
    });
  });

  await page.goto("/recommendations?userId=test-user");

  await expect(page.getByRole("heading", { name: "Testing at the Boundary" }))
    .toBeVisible();
  expect(intercepted).toBe(true);
});

O status e o contentType não estão ali para enfeitar o fixture. Eles deixam explícito o que a aplicação recebe. Adicione casos separados para lista vazia, resposta inválida e erro temporário. Não coloque todos os estados em um único teste com condicionais: cada falha deve dizer qual comportamento deixou de funcionar.

Se a tela depende de autenticação, mantenha o login no fixture de contexto e deixe o mock representar somente a API que o caso controla. Um JSON gigante copiado de produção mistura dados de setup, contrato e cenário. Comece com o menor objeto que prova a decisão visual e aumente-o somente quando a aplicação realmente lê outro campo.

Como usar route.fetch sem perder o sinal da API real?

route.fetch faz a requisição original e devolve uma resposta que pode ser alterada antes de route.fulfill. O API de Route do Playwright documenta esse caminho para cenários em que a chamada real é necessária, mas um detalhe da resposta precisa ser controlado.

test("mostra o estado vazio mesmo com uma resposta real", async ({ page }) => {
  await page.route("**/api/recommendations", async (route) => {
    const response = await route.fetch();
    const body = await response.json();

    await route.fulfill({
      response,
      json: { ...body, items: [] },
    });
  });

  await page.goto("/recommendations?userId=test-user");
  await expect(page.getByText("No recommendations yet")).toBeVisible();
});

Esse teste tem uma pergunta diferente do primeiro. Ele deixa a API participar, mas força uma variação que pode ser difícil de produzir de forma segura no ambiente. Use-o para explorar a integração e o tratamento da resposta, não como substituto de um teste determinístico que precisa rodar offline.

Há um custo: o teste agora pode falhar por autenticação, rede, limite de uso ou mudança no serviço. Por isso, deixe route.fetch em uma camada menor ou em uma execução de integração. O caminho rápido da suíte deve continuar usando route.fulfill para estados conhecidos.

Como reproduzir uma conversa com routeFromHAR?

HAR é uma boa escolha quando o fluxo depende de várias chamadas e o arquivo gravado pode ser tratado como um artefato revisável. O Playwright recomenda gravar o HAR, versioná-lo junto com o teste e desligar a atualização durante a execução normal. A correspondência considera URL e método; em POST, o payload também importa.

test("reproduz o fluxo gravado de recomendações", async ({ page }) => {
  await page.routeFromHAR("./hars/recommendations.har", {
    url: "**/api/recommendations",
    update: false,
  });

  await page.goto("/recommendations?userId=test-user");
  await expect(page.getByRole("heading", { name: "Testing at the Boundary" }))
    .toBeVisible();
});

Revise o HAR como código. Remova cookies, tokens, dados pessoais e respostas que não pertencem ao cenário. Se o teste passa apenas porque um HAR antigo responde a uma URL ampla demais, você ganhou velocidade e perdeu diagnóstico. Uma verificação útil é fazer o caso falhar quando nenhuma entrada do arquivo corresponder, em vez de deixar a aplicação seguir com uma resposta inesperada.

Como provar que o mock não escondeu o contrato?

O caminho mais simples é separar as camadas. A suíte do navegador testa o comportamento visível com route.fulfill. Uma suíte de API usa request ou um APIRequestContext independente contra staging, sandbox ou um serviço local. Se dois times evoluem o endpoint separadamente, um teste de contrato verifica as expectativas do consumidor contra o provedor, como explica o PactFlow sobre contract testing orientado pelo consumidor.

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

test("o serviço de staging mantém o contrato de recomendações", async ({ request }) => {
  const response = await request.get("/api/recommendations?userId=test-user");

  expect(response.ok()).toBe(true);
  const body = await response.json();
  expect(body).toEqual(
    expect.objectContaining({
      items: expect.any(Array),
    }),
  );
});

Esse teste precisa de um baseURL de staging e de dados que possam ser descartados. Não aponte uma suíte automática para produção só porque o endpoint é público. Se não existe sandbox, coloque a chamada real atrás de uma etapa manual ou crie um contrato explícito que o provedor possa verificar.

O resultado desejado é uma matriz pequena:

  1. route.fulfill cobre estados da interface e roda em cada alteração.
  2. route.fetch ou HAR cobre uma variação que precisa de uma conversa de rede mais parecida com a realidade.
  3. O teste de API ou contrato verifica método, autenticação, schema e estado do serviço em uma execução separada.

Não conte os três como a mesma cobertura. Eles podem usar o mesmo nome de endpoint, mas respondem a perguntas diferentes.

Quando a dependência tem uma superfície própria, use também testes de contrato para um servidor MCP em TypeScript como referência de fronteira: o transporte e o schema precisam de uma prova separada do comportamento da tela.

Quais falhas tornam esse padrão enganoso?

O primeiro erro é registrar uma rota ampla demais. **/api/** pode capturar a requisição que o teste deveria observar e devolver um sucesso para um endpoint incorreto. Use uma expressão ou glob estreito e verifique método, query e corpo quando esses dados fazem parte do contrato.

O segundo é esquecer que SSR e chamadas fora do navegador acontecem em outro processo. page.route() intercepta o tráfego do contexto do browser. Ele não corrige automaticamente uma chamada feita pelo servidor durante o render. Uma discussão recente no r/Playwright sobre APIs de Server Components chama atenção para essa fronteira. Para SSR, controle o ambiente do servidor, use um proxy de teste ou teste a camada com uma ferramenta adequada.

O terceiro é deixar um service worker esconder a requisição. A documentação de Network do Playwright explica que pode ser necessário configurar serviceWorkers: "block" quando os eventos de rede não aparecem como esperado. Primeiro confirme a topologia; depois escolha o mock.

O quarto é transformar retry em aprovação. O guia de retries do Playwright classifica como flaky o teste que falha na primeira execução e só passa depois de uma nova tentativa. Aumentar retries pode preservar o pipeline, mas não prova que o mock, o fixture ou o ambiente ficaram corretos.

Checklist para revisar um mock de API

Antes de aceitar um teste, responda às perguntas abaixo:

  • O teste diz se está verificando a UI, a integração ou o contrato?
  • A rota é registrada antes da navegação?
  • O padrão da rota inclui o método, a URL e os parâmetros relevantes?
  • O fixture contém apenas os campos que o cenário precisa?
  • Há um caso de erro com status e corpo coerentes?
  • O teste prova que a rota esperada foi interceptada?
  • Existe outro teste que chama staging, sandbox ou o provedor do contrato?
  • Cookies, tokens e dados pessoais ficaram fora do HAR e dos relatórios?
  • Uma falha de rede aparece como falha de integração, e não como uma tela verde?
  • O primeiro retry gera um sinal de flakiness em vez de ser escondido?

Se a resposta final for "não", o teste pode continuar útil. Apenas dê a ele um nome e uma posição na suíte que correspondam ao que ele realmente prova.

Perguntas frequentes

Mockar a API torna o teste E2E inútil?

Não. O mock mantém o navegador diante de estados difíceis de criar e torna a execução repetível. Ele não substitui um teste de integração ou contrato. A separação correta é usar o mock para o comportamento da UI e uma chamada independente para saber se o serviço real ainda aceita o acordo.

route.fetch é sempre melhor que route.fulfill?

Não. route.fetch preserva uma chamada real e pode herdar instabilidade, dados externos e autenticação. route.fulfill é melhor para um caso determinístico, como um erro 503 ou uma lista vazia. Escolha pelo sinal que o teste precisa, não por parecer mais próximo de produção.

Um HAR substitui um ambiente de staging?

Não. HAR reproduz o tráfego que foi gravado e ajuda a testar o cliente sem rede. Ele não demonstra que o provedor atual aceita a requisição. Versione o HAR como fixture e mantenha uma verificação separada no staging, sandbox ou contrato do serviço.

Como mockar uma API usada durante SSR?

page.route() não alcança automaticamente chamadas feitas no servidor. Controle a dependência no processo que faz o render, aponte-o para um serviço de teste ou teste a API no nível do servidor. Depois use Playwright para verificar o HTML e o comportamento que o navegador realmente recebe.

Fontes consultadas