Mockar uma requisição de rede no Playwright leva três linhas, mas a pergunta mais difícil é quais requisições mockar. Um endpoint de pagamento mockado faz o teste passar enquanto esconde exatamente o modo de falha que os usuários realmente veem. page.route() com route.fulfill() controla a resposta completa; route.fallback() permite mockar um endpoint cirurgicamente enquanto deixa tudo o mais ir ao servidor real.
Por que mockar requisições de rede
O argumento a favor de mocking não é sobre evitar testes reais. É sobre testar a coisa certa no nível certo.
Quando um teste de UI bate em uma API real, você está à mercê da latência de rede, estado do servidor e disponibilidade de terceiros. Um teste que verifica como a tabela renderiza quando o endpoint /api/items retorna um array vazio é um teste de front-end. Ele não deveria exigir um estado específico de banco de dados para passar. Mocking permite desacoplar essa preocupação por completo.
Os três motivos principais para mockar:
Velocidade é o mais óbvio. Um teste que intercepta chamadas de rede e retorna uma resposta pré-construída roda em milissegundos em vez de esperar por uma viagem de ida e volta real.
Confiabilidade é o mais importante. Testes que batem em backends reais falham por razões sem relação com o que você está testando. O ambiente de staging caiu, uma migration rodou, alguém deletou os dados de teste. Respostas mockadas são determinísticas por definição.
Estados de erro são o motivo mais subestimado. Você não consegue acionar um 503 ou um timeout de rede de forma confiável contra um servidor real numa suite de testes. Com page.route(), você produz essas condições sob demanda.
page.route(): o padrão de interceptação
page.route() recebe um padrão de URL (string, glob ou regex) e uma função handler. Toda requisição correspondente passa pelo handler antes de ir à rede.
import { test, expect } from '@playwright/test';
test('intercepta uma requisição de rede', async ({ page }) => {
await page.route('https://lab.becomeqa.com/api/items', route => {
// Handler recebe o route — você decide o que fazer com ele
console.log('Requisição interceptada:', route.request().url());
route.continue(); // Passa sem alteração
});
await page.goto('https://lab.becomeqa.com');
});O handler recebe um objeto Route com quatro métodos principais. fulfill() retorna uma resposta mockada, abort() bloqueia a requisição por completo, continue() deixa passar, e fallback() passa para o próximo handler correspondente. Você vai usar os quatro.
Padrões glob funcionam como esperado:
// Corresponde a qualquer requisição para o caminho /api/
await page.route('**/api/**', route => route.continue());
// Corresponde a um endpoint específico independente da origem
await page.route('**/api/items', route => route.continue());page.route() só intercepta requisições feitas por aquela página específica. Se precisar interceptar requisições em múltiplas páginas de um contexto, use browserContext.route().Retornando respostas JSON mockadas com fulfill()
route.fulfill() curto-circuita a requisição e retorna qualquer resposta que você especificar. Este é o principal recurso do mocking de UI.
test('tabela renderiza com dados mockados da API', async ({ page }) => {
const mockItems = [
{ id: '1', destination: 'Tokyo', status: 'planned', notes: 'Cherry blossom season' },
{ id: '2', destination: 'Lisbon', status: 'completed', notes: '' },
];
await page.route('**/api/items', route => {
route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify(mockItems),
});
});
await page.goto('https://lab.becomeqa.com');
await expect(page.getByText('Tokyo')).toBeVisible();
await expect(page.getByText('Lisbon')).toBeVisible();
});Você controla cada parte da resposta: status code, headers, content type e body. Se o app verifica headers da resposta (como Content-Type), inclua-os explicitamente.
Para payloads mockados maiores, carregue-os de um arquivo JSON de fixture:
import { readFileSync } from 'fs';
import path from 'path';
test('tabela renderiza com dados de fixture', async ({ page }) => {
const fixtureBody = readFileSync(
path.join(__dirname, 'fixtures/items.json'),
'utf-8'
);
await page.route('**/api/items', route => {
route.fulfill({
status: 200,
contentType: 'application/json',
body: fixtureBody,
});
});
await page.goto('https://lab.becomeqa.com');
await expect(page.getByRole('table')).toBeVisible();
});Isso mantém os arquivos de teste legíveis quando os dados mockados são complexos. Um registro de pagamento realista ou um perfil de usuário profundamente aninhado não pertence ao código inline.
Bloqueando requisições com abort()
Às vezes você quer verificar o que acontece quando uma requisição não consegue completar. Pode ser uma imagem que falha ao carregar, um script de analytics de terceiro que dá timeout, ou uma chamada de API não crítica que o app deve tratar graciosamente.
test('app mostra estado de erro quando a API está inacessível', async ({ page }) => {
await page.route('**/api/items', route => {
route.abort('failed'); // Simula uma falha de conexão
});
await page.goto('https://lab.becomeqa.com');
await expect(page.getByText('Unable to load items')).toBeVisible();
await expect(page.getByRole('table')).not.toBeVisible();
});O método abort() aceita um código de erro: 'failed' para um erro de conexão genérico, 'timedout' para comportamento de timeout, 'blockedbyclient' para simular um bloqueio no estilo de ad-blocker. Use 'timedout' para testar tratamento de timeout especificamente:
test('mostra mensagem de timeout após resposta lenta', async ({ page }) => {
await page.route('**/api/items', route => {
route.abort('timedout');
});
await page.goto('https://lab.becomeqa.com');
await expect(page.getByText('Request timed out')).toBeVisible();
});Bloquear também é útil para acelerar testes descartando requisições que você sabe que são irrelevantes. Bloquear analytics de terceiros, CDNs de fontes ou scripts de rastreamento pode economizar segundos das suites de teste:
test.beforeEach(async ({ page }) => {
// Bloqueia requisições de analytics — são irrelevantes e lentas
await page.route(/google-analytics\.com|segment\.io/, route => route.abort());
});Modificando requisições em trânsito com continue()
route.continue() passa a requisição para o servidor mas permite sobrescrever qualquer parte dela primeiro: URL, método, headers ou body. Útil para injetar headers de autenticação sem modificar o código do app, ou para testar como o backend lida com combinações específicas de headers.
test('injeta header de auth em toda requisição de API', async ({ page }) => {
await page.route('**/api/**', route => {
route.continue({
headers: {
...route.request().headers(),
'Authorization': 'Bearer test-token-for-e2e',
},
});
});
await page.goto('https://lab.becomeqa.com/dashboard');
await expect(page.getByRole('table')).toBeVisible();
});Você também pode reescrever a URL da requisição, o que é útil para redirecionar chamadas de API de produção para um ambiente de staging sem mudar nenhuma config:
test('redireciona chamadas de API para staging', async ({ page }) => {
await page.route('https://api.production.com/**', route => {
const newUrl = route.request().url().replace(
'api.production.com',
'api.staging.becomeqa.com'
);
route.continue({ url: newUrl });
});
await page.goto('https://lab.becomeqa.com');
});continue() com headers customizados, sempre faça spread de route.request().headers() primeiro. Substituir os headers por completo vai descartar coisas como Content-Type e Accept que o servidor pode exigir.Interceptando e inspecionando com waitForRequest e waitForResponse
Às vezes o objetivo não é mockar nada. É verificar que uma requisição específica realmente aconteceu, ou capturar os dados da resposta para assertion. page.waitForRequest() e page.waitForResponse() retornam promises que resolvem quando uma requisição ou resposta correspondente é vista.
test('clicar em Salvar envia o payload correto', async ({ page }) => {
await page.goto('https://lab.becomeqa.com');
// Configura o listener ANTES de disparar a ação
const requestPromise = page.waitForRequest(request =>
request.url().includes('/api/items') && request.method() === 'POST'
);
await page.getByRole('button', { name: 'Add Item' }).click();
await page.getByLabel('Destination').fill('Berlin');
await page.getByRole('button', { name: 'Save' }).click();
const request = await requestPromise;
const payload = request.postDataJSON();
expect(payload.destination).toBe('Berlin');
expect(payload.status).toBeDefined();
});O detalhe crítico: configure o listener antes de disparar a ação. Se você awaita o clique no botão primeiro e depois awaita waitForRequest, a requisição pode já ter disparado e você vai esperar por uma que nunca vai chegar.
waitForResponse funciona da mesma forma, mas resolve com a resposta:
test('formulário de pagamento mostra mensagem de sucesso após confirmação da API', async ({ page }) => {
await page.goto('https://lab.becomeqa.com/payment');
const responsePromise = page.waitForResponse(response =>
response.url().includes('/api/payments') && response.status() === 200
);
await page.getByLabel('Card Number').fill('4111111111111111');
await page.getByRole('button', { name: 'Pay' }).click();
const response = await responsePromise;
const body = await response.json();
expect(body.status).toBe('success');
await expect(page.getByText('Payment confirmed')).toBeVisible();
});Testando estados de erro: 500s, 401s e timeouts
Teste de estados de erro é onde page.route() realmente se justifica. São cenários quase impossíveis de acionar de forma confiável contra um backend real, mas diretos de mockar.
test('mostra banner de erro em falha do servidor', async ({ page }) => {
await page.route('**/api/items', route => {
route.fulfill({
status: 500,
contentType: 'application/json',
body: JSON.stringify({ error: 'Internal server error' }),
});
});
await page.goto('https://lab.becomeqa.com');
await expect(page.getByRole('alert')).toContainText('Something went wrong');
});test('redireciona para login quando a sessão expira', async ({ page }) => {
let requestCount = 0;
await page.route('**/api/items', route => {
requestCount++;
if (requestCount === 1) {
// Primeira requisição tem sucesso — usuário está "logado"
route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify([{ id: '1', destination: 'Madrid', status: 'planned' }]),
});
} else {
// Requisições seguintes retornam 401 — sessão expirada
route.fulfill({
status: 401,
contentType: 'application/json',
body: JSON.stringify({ error: 'Session expired' }),
});
}
});
await page.goto('https://lab.becomeqa.com');
await page.reload();
await expect(page).toHaveURL(/\/login/);
});test('mostra botão de retry após timeout de rede', async ({ page }) => {
await page.route('**/api/items', async route => {
// Delay depois abort — simula uma rede lenta que dá timeout
await new Promise(resolve => setTimeout(resolve, 8000));
route.abort('timedout');
});
await page.goto('https://lab.becomeqa.com');
await expect(page.getByRole('button', { name: 'Retry' })).toBeVisible({
timeout: 15000,
});
});route.fallback() para mocking cirúrgico
route.fallback() permite que um handler dê passagem e deixe o próximo handler correspondente (ou a rede real) assumir. É a ferramenta certa quando você quer mockar endpoints específicos enquanto deixa tudo o mais ir ao servidor real.
test('mocka apenas o endpoint de pagamentos', async ({ page }) => {
// Primeiro handler: mocka o endpoint de pagamentos
await page.route('**/api/payments', route => {
route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({ status: 'success', transactionId: 'mock-txn-001' }),
});
});
// Segundo handler: deixa tudo o mais passar
await page.route('**', route => route.fallback());
await page.goto('https://lab.becomeqa.com');
// Login real, carregamento de dados real — só pagamentos é mockado
await page.getByRole('button', { name: 'Login' }).click();
await page.getByLabel('Username').fill('admin@becomeqa.com');
await page.getByLabel('Password').fill('testpass123');
await page.getByRole('button', { name: 'Submit' }).click();
await page.goto('https://lab.becomeqa.com/payment');
await page.getByRole('button', { name: 'Pay' }).click();
await expect(page.getByText('Payment confirmed')).toBeVisible();
});route.fallback() é o que separa o mocking cirúrgico do mocking de tudo ou nada. Você pode trocar uma dependência externa frágil enquanto mantém o resto do teste com comportamento real.
Múltiplos handlers para o mesmo route são avaliados em ordem inversa de registro. O último registrado é o primeiro avaliado. Quando um handler chama fallback(), o Playwright passa para o handler registrado anteriormente.
// Registrado primeiro — age como padrão
await page.route('**/api/**', route => route.continue());
// Registrado segundo — avaliado primeiro
await page.route('**/api/payments', route => {
route.fulfill({ status: 200, body: JSON.stringify({ status: 'success' }) });
});Quando NÃO mockar
Mocking tem um custo: seus testes são tão bons quanto seus dados mockados. Se a API real retorna uma estrutura que você não antecipou na sua fixture, seus testes mockados vão passar enquanto a produção quebra.
Mockar ativamente prejudica nestas categorias de testes:
Testes de contrato precisam bater na API real. Se você está verificando que uma requisição do front-end corresponde ao contrato esperado do backend (nomes de campos, headers obrigatórios, estrutura da resposta), um mock não consegue detectar a incompatibilidade. É exatamente para isso que a API real existe. Testes de integração para fluxos críticos devem usar o backend real. O fluxo de login, o fluxo de pagamento, a submissão de dados que move o negócio principal precisam de integração real para capturar os modos de falha reais. Mocke-os e você está testando confiança, não comportamento. Ao debugar um bug real, mockar impede você de ver o problema real. Se usuários estão reportando um problema na página de confirmação de pagamento, a última coisa que você quer é um endpoint de pagamento mockado escondendo a resposta real.A regra prática: mocke para tornar um teste determinístico e rápido quando a requisição de rede não é o que você está testando. Não mocke quando a própria requisição, ou o servidor que a processa, está sob teste. Uma suite de testes completa usa os dois: chamadas reais de API para testes de integração e contratos, respostas mockadas para testes de renderização de UI e estados de erro.
FAQ
page.route() afeta requisições que começam antes de o handler ser registrado?
Não. Handlers só interceptam requisições iniciadas após o registro. Sempre chame page.route() antes de page.goto() ou antes da ação que dispara a requisição.
Sim. O Playwright avalia handlers em ordem inversa de registro. Use route.fallback() para passar o controle para o próximo handler. Use page.unroute() para remover um handler quando não precisar mais dele.
route.fulfill() não suporta streaming nativamente. Ele envia o body completo de uma vez. Para cenários de streaming, você vai precisar de um servidor de teste local ou de uma ferramenta como msw (Mock Service Worker) integrada junto com o Playwright.
Dados mockados devem ficar nos arquivos de teste ou em arquivos de fixture?
Objetos mockados curtos (2-3 campos) são adequados inline. Qualquer coisa maior ou reutilizada entre testes pertence a um diretório fixtures/. Isso mantém os arquivos de teste focados em comportamento, não em configuração de dados.
page.route() e Service Workers para mocking?
page.route() intercepta no nível do Playwright, antes do stack de rede do navegador. Service Workers interceptam dentro do navegador. Para testes Playwright, page.route() é mais simples, mais confiável e não requer configuração no código do app. Service Workers são úteis quando você precisa que o mock persista em navegações completas de página ou afete o cache do service worker.
→ Veja também: Testes de API com Playwright: Além da Interface | Construindo um Framework de Testes Playwright Escalável do Zero | Testes de API com o APIRequestContext do Playwright (Sem Postman)