Quando um teste de UI deleta um recurso e verifica a interface, ele só confirma que o frontend atualizou. A chamada de API pode ter falhado silenciosamente enquanto a UI mostrava um estado de sucesso otimista. Assertar o DELETE no nível de API depois pega essa falha. O APIRequestContext é o cliente HTTP nativo do Playwright, disponível pela fixture request no mesmo pacote @playwright/test que você já tem instalado.
O que é APIRequestContext e quando usar
O APIRequestContext é o cliente HTTP nativo do Playwright. Ele permite enviar requisições GET, POST, PUT, PATCH e DELETE, inspecionar respostas e manipular headers e cookies. Assertions sobre status codes e bodies de resposta ficam dentro do mesmo arquivo .spec.ts.
Não é uma substituição para testes de UI. Serve a um propósito diferente. Um teste de UI controla o navegador: clica em botões, preenche formulários, espera elementos. Um teste com APIRequestContext envia requisições HTTP diretamente para o servidor, pulando o navegador completamente. Isso o torna mais rápido, mais confiável e mais adequado para testar a camada de backend.
Quando usar APIRequestContext em vez de teste de UI:
- Você está testando validação de dados. A API rejeita um campo obrigatório ausente?
- Você está testando autenticação. Um 401 é retornado quando não há token?
- Você está testando lógica de negócio que vive no servidor, não na UI.
- Você quer fazer seed de dados de teste antes de um teste de UI sem passar pelo formulário.
- Você quer verificar um efeito colateral do backend após uma ação na UI.
Quando manter os testes de UI? Quando você está testando o que o usuário realmente vê e interage: renderização, navegação, comportamento de formulários, feedback visual. As duas camadas pertencem a uma suite completa. O erro é usar uma onde a outra é claramente melhor.
A fixture request
O Playwright expõe o APIRequestContext pela fixture nativa request. Você a usa exatamente como usa page: declare na assinatura da função de teste e o Playwright cuida do setup.
import { test, expect } from '@playwright/test';
test('GET /api/items retorna 200', async ({ request }) => {
const response = await request.get('https://lab.becomeqa.com/api/items');
expect(response.status()).toBe(200);
});Nenhuma janela de navegador abre. Nenhum DOM renderiza. O test runner envia uma requisição HTTP, recebe uma resposta e suas assertions rodam. O processo completo termina em menos de 100 milissegundos numa conexão típica.
A fixture request cria um APIRequestContext isolado para cada teste. Tem seu próprio cookie jar, seus próprios headers e nenhuma conexão com nenhum contexto de navegador. Esse isolamento é intencional: seus testes de API são independentes do que o navegador está fazendo.
Fazendo requisições GET e assertando respostas
Um teste GET tem três partes: enviar a requisição, verificar o status, verificar o body.
import { test, expect } from '@playwright/test';
test('GET /api/items retorna uma lista válida', async ({ request }) => {
const response = await request.get('https://lab.becomeqa.com/api/items');
// Verificação de status
expect(response.status()).toBe(200);
expect(response.ok()).toBeTruthy(); // atalho: true para qualquer 2xx
// Parsear o body JSON
const items = await response.json();
// Verificações de estrutura
expect(Array.isArray(items)).toBe(true);
expect(items.length).toBeGreaterThan(0);
// Verificar as propriedades de um item
const first = items[0];
expect(first).toHaveProperty('id');
expect(first).toHaveProperty('destination');
expect(first).toHaveProperty('status');
});response.ok() retorna true para qualquer status code na faixa 200–299. Use-o quando só precisa confirmar sucesso sem se importar com o código exato. Use response.status() quando o código específico importa: 200, 201 e 204 têm significados diferentes.
Você também pode ler o body como texto ou como buffer:
const text = await response.text();
const buffer = await response.body(); // BufferPara assertar sobre os headers da resposta:
test('resposta inclui content-type JSON', async ({ request }) => {
const response = await request.get('https://lab.becomeqa.com/api/items');
const headers = response.headers();
expect(headers['content-type']).toContain('application/json');
});Requisições POST com body JSON
Requisições POST enviam dados para o servidor. Passe o payload via opção data e o Playwright automaticamente serializa para JSON e define Content-Type: application/json.
test('POST /api/items cria um recurso', async ({ request }) => {
const response = await request.post('https://lab.becomeqa.com/api/items', {
data: {
destination: 'Kyoto',
status: 'planned',
notes: 'Visitar o bosque de bambu de Arashiyama'
}
});
// Uma API bem projetada retorna 201 Created para novos recursos
expect(response.status()).toBe(201);
const created = await response.json();
expect(created).toHaveProperty('id');
expect(created.destination).toBe('Kyoto');
expect(created.status).toBe('planned');
});Guarde o id retornado quando precisar fazer limpeza ou encadear requisições:
test('criar e depois deletar um item', async ({ request }) => {
const createRes = await request.post('https://lab.becomeqa.com/api/items', {
data: { destination: 'Tbilisi', status: 'planned' }
});
expect(createRes.status()).toBe(201);
const { id } = await createRes.json();
const deleteRes = await request.delete(`https://lab.becomeqa.com/api/items/${id}`);
expect(deleteRes.status()).toBe(204);
// Confirmar que sumiu
const getRes = await request.get(`https://lab.becomeqa.com/api/items/${id}`);
expect(getRes.status()).toBe(404);
});Para dados codificados como formulário, troque data por form:
const response = await request.post('https://lab.becomeqa.com/api/login', {
form: {
username: 'admin@becomeqa.com',
password: 'testpass123'
}
});Autenticação: Bearer tokens e headers customizados
A maioria das APIs reais usa autenticação. Os dois padrões mais comuns são Bearer tokens e API keys passadas em headers.
Bearer token: fazer login primeiro, depois usar o token.test('requisição autenticada com Bearer token', async ({ request }) => {
// Passo 1: obter um token
const loginRes = await request.post('https://lab.becomeqa.com/api/auth/login', {
data: {
username: 'admin@becomeqa.com',
password: 'testpass123'
}
});
expect(loginRes.status()).toBe(200);
const { token } = await loginRes.json();
// Passo 2: usá-lo nas requisições seguintes
const itemsRes = await request.get('https://lab.becomeqa.com/api/items', {
headers: {
Authorization: `Bearer ${token}`
}
});
expect(itemsRes.status()).toBe(200);
});Quando o mesmo token é usado em muitos testes, mova o passo de login para um bloco beforeAll e compartilhe o token pela suite:
import { test, expect } from '@playwright/test';
let authToken: string;
test.beforeAll(async ({ request }) => {
const res = await request.post('https://lab.becomeqa.com/api/auth/login', {
data: {
username: 'admin@becomeqa.com',
password: 'testpass123'
}
});
const body = await res.json();
authToken = body.token;
});
test('GET items como usuário autenticado', async ({ request }) => {
const response = await request.get('https://lab.becomeqa.com/api/items', {
headers: { Authorization: `Bearer ${authToken}` }
});
expect(response.status()).toBe(200);
});playwright.config.ts.
// playwright.config.ts
import { defineConfig } from '@playwright/test';
export default defineConfig({
use: {
baseURL: 'https://lab.becomeqa.com',
extraHTTPHeaders: {
'X-Api-Key': process.env.API_KEY ?? ''
}
}
});Toda requisição que a fixture request enviar incluirá esse header automaticamente. Sem precisar repetir em cada teste.
.env localmente (com dotenv) e secrets do GitHub Actions no CI. O acesso ao process.env do Playwright funciona da mesma forma nos dois ambientes.playwright.request.newContext() para testes de API standalone
A fixture request é conveniente dentro dos testes, mas às vezes você precisa de um APIRequestContext fora do test runner: em um arquivo de setup global, em um script utilitário, ou quando quer um contexto com configuração própria separado do padrão.
playwright.request.newContext() cria um contexto standalone que você controla explicitamente:
// global-setup.ts
import { chromium, request } from '@playwright/test';
async function globalSetup() {
// Criar um contexto de API standalone
const apiContext = await request.newContext({
baseURL: 'https://lab.becomeqa.com',
extraHTTPHeaders: {
'Content-Type': 'application/json'
}
});
// Fazer seed de dados antes de qualquer teste rodar
await apiContext.post('/api/items', {
data: { destination: 'Lisboa', status: 'planned' }
});
// Sempre dispose quando terminar
await apiContext.dispose();
}
export default globalSetup;Referencie o arquivo de setup global no config:
// playwright.config.ts
export default defineConfig({
globalSetup: './global-setup.ts',
use: {
baseURL: 'https://lab.becomeqa.com'
}
});newContext() aceita as mesmas opções que o bloco de config use: baseURL, extraHTTPHeaders, httpCredentials, ignoreHTTPSErrors. Chame dispose() quando o contexto não for mais necessário. Ele fecha conexões abertas e limpa cookies.
playwright.request.newContext() e a fixture request criam instâncias de APIRequestContext. A diferença é o ciclo de vida: a fixture é criada e descartada automaticamente por teste. newContext() dá controle manual, útil para setup global, scripts de teardown ou contextos que abrangem múltiplos testes.Combinando setup via API com verificação na UI
É aqui que o APIRequestContext entrega o maior retorno. A parte mais lenta e frágil de um teste de UI geralmente é o setup. Preencher formulários, esperar por estado e navegar por telas só para chegar ao cenário que você quer testar. Substitua isso com uma chamada de API.
test('item criado via API aparece na lista da UI', async ({ page, request }) => {
// Setup rápido e confiável. Sem navegador envolvido.
const createRes = await request.post('https://lab.becomeqa.com/api/items', {
data: { destination: 'Porto', status: 'planned' }
});
expect(createRes.status()).toBe(201);
const { id } = await createRes.json();
// Agora teste o que importa: a UI exibe corretamente?
await page.goto('https://lab.becomeqa.com/items');
await expect(page.getByText('Porto')).toBeVisible();
await expect(page.getByTestId(`item-${id}`)).toBeVisible();
// Limpeza via API. Também mais rápido do que clicar em um fluxo de delete na UI.
await request.delete(`https://lab.becomeqa.com/api/items/${id}`);
});test('deletar item via UI o remove do banco', async ({ page, request }) => {
// Criar via API
const createRes = await request.post('https://lab.becomeqa.com/api/items', {
data: { destination: 'Valletta', status: 'planned' }
});
const { id } = await createRes.json();
// Deletar pela UI. É isso que estamos testando de verdade.
await page.goto('https://lab.becomeqa.com/items');
await page.getByTestId(`item-${id}`).getByRole('button', { name: 'Delete' }).click();
await page.getByRole('button', { name: 'Confirm' }).click();
// Verificar o estado do backend, não só o estado da UI
const checkRes = await request.get(`https://lab.becomeqa.com/api/items/${id}`);
expect(checkRes.status()).toBe(404);
});Esse segundo padrão é pouco usado. Quando um usuário deleta algo, a UI pode atualizar de forma otimista e parecer correta mesmo que a chamada de API tenha falhado. Assertar no nível de API captura essa falha.
Helpers de API reutilizáveis e fixtures
Repetir request.post('/api/auth/login', ...) em cada arquivo de teste é ruído. Construa uma pequena classe helper e exponha-a por uma fixture customizada.
Primeiro, a classe helper:
// lib/api-client.ts
import { APIRequestContext } from '@playwright/test';
export class ApiClient {
constructor(private request: APIRequestContext) {}
async login(username: string, password: string): Promise<string> {
const res = await this.request.post('/api/auth/login', {
data: { username, password }
});
const { token } = await res.json();
return token;
}
async createItem(data: { destination: string; status: string; notes?: string }) {
const res = await this.request.post('/api/items', { data });
expect(res.status()).toBe(201);
return res.json();
}
async deleteItem(id: string) {
await this.request.delete(`/api/items/${id}`);
}
async getItem(id: string) {
return this.request.get(`/api/items/${id}`);
}
}Depois exponha por uma fixture customizada:
// fixtures.ts
import { test as base } from '@playwright/test';
import { ApiClient } from './lib/api-client';
type Fixtures = {
api: ApiClient;
};
export const test = base.extend<Fixtures>({
api: async ({ request }, use) => {
const client = new ApiClient(request);
await use(client);
}
});
export { expect } from '@playwright/test';Os testes ficam muito mais legíveis:
import { test, expect } from './fixtures';
test('criar e verificar item', async ({ api, page }) => {
const item = await api.createItem({ destination: 'Riga', status: 'planned' });
await page.goto('https://lab.becomeqa.com/items');
await expect(page.getByText('Riga')).toBeVisible();
await api.deleteItem(item.id);
});O helper encapsula a lógica de requisições. A fixture gerencia o ciclo de vida. O teste foca no cenário. Cada camada tem uma responsabilidade.
expect fora da classe ApiClient, exceto para pré-condições obrigatórias como verificar status 201 após um create. Assertions em helpers tornam as falhas mais difíceis de rastrear porque a stack aponta para o helper, não para o teste.request vs page.request: qual a diferença
Os dois são instâncias de APIRequestContext. A distinção é como tratam cookies e estado de sessão.
request, a fixture, é um contexto isolado. Tem seu próprio cookie jar, separado de qualquer navegador. Não compartilha estado com page. Quando você faz login via request, o navegador não sabe.
page.request está vinculado ao contexto de navegador ao qual page pertence. Compartilha cookies com a página. Se o usuário faz login pelo navegador, page.request carrega esses cookies. Se page.request define um cookie, o navegador vê.
test('diferença entre request e page.request', async ({ page, request }) => {
// Fazer login pelo navegador
await page.goto('https://lab.becomeqa.com/login');
await page.getByLabel('Username').fill('admin@becomeqa.com');
await page.getByLabel('Password').fill('testpass123');
await page.getByRole('button', { name: 'Login' }).click();
// page.request carrega o cookie de auth, retorna 200
const withCookies = await page.request.get('https://lab.becomeqa.com/api/items');
expect(withCookies.status()).toBe(200);
// fixture request não tem cookie, retorna 401
const withoutCookies = await request.get('https://lab.becomeqa.com/api/items');
expect(withoutCookies.status()).toBe(401);
});Qual usar? Se o teste envolve uma sessão de navegador e chamadas de API que devem usar a mesma autenticação, use page.request. Para testes de API puros sem navegador, use a fixture request. Se precisar de um contexto totalmente independente com headers ou base URL customizados, use playwright.request.newContext().
FAQ
Ainda preciso do Postman?Postman é uma boa ferramenta de exploração. Quando você encontra uma API pela primeira vez e não conhece sua estrutura, abra o Postman, explore, leia as respostas, descubra o que precisa. Uma vez que você sabe o que vai testar, escreva no Playwright. Você tem controle de versão, integração com CI e a capacidade de combinar assertions de API e UI no mesmo teste, nada disso o Postman oferece.
Posso usarAPIRequestContext para testar GraphQL?
Sim. GraphQL sobre HTTP é uma requisição POST para um único endpoint com body JSON contendo query e opcionalmente variables. A opção data lida com isso diretamente:
const response = await request.post('https://lab.becomeqa.com/graphql', {
data: {
query: `
query GetItem($id: ID!) {
item(id: $id) {
id
destination
status
}
}
`,
variables: { id: '123' }
}
});
const { data } = await response.json();
expect(data.item.destination).toBe('Kyoto');APIRequestContext segue redirects automaticamente?
Sim, por padrão segue até 20 redirects. Para desabilitar e inspecionar a resposta de redirect diretamente, passe maxRedirects: 0 nas opções da requisição.
Configure um timeout customizado nas opções da requisição ou aumente o timeout no playwright.config.ts para o projeto de API. Para APIs com rate limiting em testes, considere fazer seed dos dados no globalSetup uma vez em vez de criar a cada teste.
response.json() e response.text()?
response.json() parseia o body e retorna um objeto JavaScript. Lança erro se o body não é JSON válido. response.text() retorna a string bruta. Use text() para debugging ou quando o endpoint retorna um formato não-JSON como texto puro ou XML.
→ Veja também: Fixtures do Playwright Explicadas: Das Integradas às Personalizadas | Configuração e Limpeza Global no Playwright | Testes de API com Playwright: Além da Interface | Testes de API Avançados com Playwright: Padrões para Projetos Reais | Autenticação em Testes de API: Chaves API, Tokens Bearer, OAuth2, JWT