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(); // Buffer

Para 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);
});

API key via header comum: configure uma vez no 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.

Guarde segredos em variáveis de ambiente, nunca hardcoded. Use um arquivo .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.

API cria os dados, UI verifica como renderiza:

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}`);
});

UI executa a ação, API confirma o efeito colateral:

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.

Mantenha chamadas de 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 usar APIRequestContext 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');

O 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.

Como lidar com testes de API que são lentos ou têm rate limiting?

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.

Qual a diferença entre 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