Um status 200 com nomes de campos errados ainda é um bug, e a maioria das suites básicas de teste de API perde isso porque só verifica o status. Em escala, os padrões que mantêm suites confiáveis: autenticação via fixture, validação de schema contra tipos de campo e campos obrigatórios, e cobertura completa do ciclo CRUD. O CRUD deve verificar que a deleção é real, não apenas uma resposta 204.

O fixture request vs page.request

Duas formas de fazer chamadas de API no Playwright:

// 1. O fixture request — sem navegador, API pura
test('teste de API', async ({ request }) => {
  const response = await request.get('/api/users');
});

// 2. page.request — compartilha cookies com a página do navegador
test('API após login pelo navegador', async ({ page }) => {
  await page.goto('/login');
  await page.fill('[data-testid="email"]', 'user@test.com');
  await page.fill('[data-testid="password"]', 'ValidPass1');
  await page.click('[data-testid="submit"]');
  
  // Essa requisição usa a sessão do navegador (cookies definidos pelo login)
  const response = await page.request.get('/api/user/profile');
});

Use o fixture request para testes de API puros. Use page.request quando precisar da sessão autenticada do navegador.

Padrões de autenticação

Auth baseada em token

test.describe('Testes de API autenticados', () => {
  let authToken: string;
  
  test.beforeAll(async ({ request }) => {
    const response = await request.post('/api/auth/login', {
      data: {
        email: 'admin@test.com',
        password: 'AdminPass1',
      },
    });
    
    expect(response.ok()).toBeTruthy();
    const body = await response.json();
    authToken = body.token;
  });
  
  test('obter lista de usuários', async ({ request }) => {
    const response = await request.get('/api/users', {
      headers: { Authorization: `Bearer ${authToken}` },
    });
    
    expect(response.status()).toBe(200);
  });
  
  test('criar usuário', async ({ request }) => {
    const response = await request.post('/api/users', {
      data: { email: 'novo@test.com', password: 'Pass1', role: 'member' },
      headers: { Authorization: `Bearer ${authToken}` },
    });
    
    expect(response.status()).toBe(201);
  });
});

Usando fixtures para auth

Melhor que beforeAll: fixtures são mais limpas e lidam com teardown:

// fixtures/index.ts
export const test = base.extend<{ authToken: string }>({
  authToken: async ({ request }, use) => {
    const response = await request.post('/api/auth/login', {
      data: { email: 'admin@test.com', password: 'AdminPass1' },
    });
    const { token } = await response.json();
    await use(token);
    // Sem teardown necessário para tokens: expiram ou são revogados naturalmente
  },
});

Validação de schema

Verifique a estrutura da resposta, não apenas os status codes:

import Ajv from 'ajv';

const ajv = new Ajv();

const userSchema = {
  type: 'object',
  required: ['id', 'email', 'role', 'createdAt'],
  properties: {
    id: { type: 'number' },
    email: { type: 'string', format: 'email' },
    role: { type: 'string', enum: ['admin', 'member', 'viewer'] },
    createdAt: { type: 'string' },
    name: { type: 'string' },
  },
  additionalProperties: false,
};

test('resposta de usuário corresponde ao schema', async ({ request, authToken }) => {
  const response = await request.get('/api/users/1', {
    headers: { Authorization: `Bearer ${authToken}` },
  });
  
  const body = await response.json();
  const validate = ajv.compile(userSchema);
  const valid = validate(body);
  
  if (!valid) {
    throw new Error(`Validação de schema falhou: ${JSON.stringify(validate.errors)}`);
  }
  
  expect(valid).toBe(true);
});

Sem biblioteca, use verificações manuais:

function validateUserSchema(body: unknown) {
  const user = body as Record<string, unknown>;
  
  expect(typeof user.id).toBe('number');
  expect(typeof user.email).toBe('string');
  expect(user.email).toMatch(/@/);
  expect(['admin', 'member', 'viewer']).toContain(user.role);
  expect(typeof user.createdAt).toBe('string');
  expect(() => new Date(user.createdAt as string)).not.toThrow();
}

test('validação de schema de usuário', async ({ request, authToken }) => {
  const response = await request.get('/api/users/1', {
    headers: { Authorization: `Bearer ${authToken}` },
  });
  
  const body = await response.json();
  validateUserSchema(body);
});

Ciclo de vida de testes CRUD

Teste o ciclo completo de criar, ler, atualizar e deletar:

test.describe('CRUD de Usuário', () => {
  const authHeaders = () => ({
    Authorization: `Bearer ${authToken}`,
  });
  
  let createdUserId: number;
  const userData = {
    email: `crud_test_${Date.now()}@test.com`,
    password: 'ValidPass1',
    name: 'Usuário de Teste CRUD',
    role: 'member',
  };
  
  test('CRIAR — POST /api/users', async ({ request }) => {
    const response = await request.post('/api/users', {
      data: userData,
      headers: authHeaders(),
    });
    
    expect(response.status()).toBe(201);
    
    const body = await response.json();
    expect(body.email).toBe(userData.email);
    expect(body.id).toBeDefined();
    expect(body.password).toBeUndefined();  // Nunca deve retornar a senha
    
    createdUserId = body.id;
  });
  
  test('LER — GET /api/users/:id', async ({ request }) => {
    const response = await request.get(`/api/users/${createdUserId}`, {
      headers: authHeaders(),
    });
    
    expect(response.status()).toBe(200);
    
    const body = await response.json();
    expect(body.id).toBe(createdUserId);
    expect(body.email).toBe(userData.email);
  });
  
  test('ATUALIZAR — PUT /api/users/:id', async ({ request }) => {
    const response = await request.put(`/api/users/${createdUserId}`, {
      data: { name: 'Nome Atualizado' },
      headers: authHeaders(),
    });
    
    expect(response.status()).toBe(200);
    
    const body = await response.json();
    expect(body.name).toBe('Nome Atualizado');
    expect(body.email).toBe(userData.email);  // E-mail inalterado
  });
  
  test('DELETAR — DELETE /api/users/:id', async ({ request }) => {
    const response = await request.delete(`/api/users/${createdUserId}`, {
      headers: authHeaders(),
    });
    
    expect(response.status()).toBe(204);  // Sem conteúdo
  });
  
  test('VERIFICAR DELETADO — GET retorna 404', async ({ request }) => {
    const response = await request.get(`/api/users/${createdUserId}`, {
      headers: authHeaders(),
    });
    
    expect(response.status()).toBe(404);
  });
});

Testando casos de erro

Não teste apenas o caminho principal:

test.describe('Tratamento de erros', () => {
  test('401 — token de auth faltando', async ({ request }) => {
    const response = await request.get('/api/users');
    expect(response.status()).toBe(401);
    
    const body = await response.json();
    expect(body.error).toBeDefined();
  });
  
  test('403 — permissões insuficientes', async ({ request, memberToken }) => {
    // Membro tentando acessar endpoint de admin
    const response = await request.get('/api/admin/logs', {
      headers: { Authorization: `Bearer ${memberToken}` },
    });
    expect(response.status()).toBe(403);
  });
  
  test('404 — recurso inexistente', async ({ request, authToken }) => {
    const response = await request.get('/api/users/999999', {
      headers: { Authorization: `Bearer ${authToken}` },
    });
    expect(response.status()).toBe(404);
    
    const body = await response.json();
    expect(body.message).toContain('not found');
  });
  
  test('400 — corpo de requisição inválido', async ({ request, authToken }) => {
    const response = await request.post('/api/users', {
      data: { email: 'nao-e-um-email', password: '123' },  // Inválido
      headers: { Authorization: `Bearer ${authToken}` },
    });
    
    expect(response.status()).toBe(400);
    
    const body = await response.json();
    expect(body.errors).toBeDefined();
    expect(body.errors).toBeInstanceOf(Array);
  });
  
  test('409 — recurso duplicado', async ({ request, authToken }) => {
    const userData = { email: 'duplicado@test.com', password: 'ValidPass1' };
    
    // Criar primeiro
    await request.post('/api/users', {
      data: userData,
      headers: { Authorization: `Bearer ${authToken}` },
    });
    
    // Criar duplicado
    const response = await request.post('/api/users', {
      data: userData,
      headers: { Authorization: `Bearer ${authToken}` },
    });
    
    expect(response.status()).toBe(409);
  });
});

Testes de paginação

test.describe('Paginação', () => {
  test('retorna tamanho de página correto', async ({ request, authToken }) => {
    const response = await request.get('/api/users?page=1&limit=10', {
      headers: { Authorization: `Bearer ${authToken}` },
    });
    
    const body = await response.json();
    
    expect(body.data).toHaveLength(10);
    expect(body.page).toBe(1);
    expect(body.limit).toBe(10);
    expect(typeof body.total).toBe('number');
    expect(typeof body.totalPages).toBe('number');
  });
  
  test('última página tem menos itens', async ({ request, authToken }) => {
    // Obter contagem total primeiro
    const firstResp = await request.get('/api/users?page=1&limit=10', {
      headers: { Authorization: `Bearer ${authToken}` },
    });
    const { total, totalPages } = await firstResp.json();
    
    // Obter última página
    const lastResp = await request.get(`/api/users?page=${totalPages}&limit=10`, {
      headers: { Authorization: `Bearer ${authToken}` },
    });
    const lastPage = await lastResp.json();
    
    const expectedLastPageCount = total % 10 || 10;
    expect(lastPage.data).toHaveLength(expectedLastPageCount);
  });
  
  test('página além do total retorna vazio', async ({ request, authToken }) => {
    const response = await request.get('/api/users?page=9999&limit=10', {
      headers: { Authorization: `Bearer ${authToken}` },
    });
    
    const body = await response.json();
    expect(body.data).toHaveLength(0);
  });
});

Combinando setup via API com testes de UI

O padrão mais poderoso: usar a API para configurar estado, depois verificar pela UI:

test('usuário recém-criado aparece no painel admin', async ({ page, request, authToken }) => {
  // 1. Criar usuário via API (rápido, confiável)
  const userData = { 
    email: `novo_${Date.now()}@test.com`, 
    password: 'ValidPass1',
    name: 'Novo Usuário API',
    role: 'member',
  };
  
  const createResp = await request.post('/api/users', {
    data: userData,
    headers: { Authorization: `Bearer ${authToken}` },
  });
  const { id } = await createResp.json();
  
  // 2. Verificar pela UI (o que os usuários realmente veem)
  await page.goto('/login');
  await page.fill('[data-testid="email"]', 'admin@test.com');
  await page.fill('[data-testid="password"]', 'AdminPass1');
  await page.click('[data-testid="submit"]');
  
  await page.goto('/admin/users');
  await page.fill('[data-testid="search"]', userData.email);
  
  await expect(page.getByTestId('user-row').first()).toContainText(userData.name);
  
  // 3. Limpeza via API (rápido, confiável)
  await request.delete(`/api/users/${id}`, {
    headers: { Authorization: `Bearer ${authToken}` },
  });
});

Assertions de tempo de resposta

test('endpoint de lista responde dentro do SLA', async ({ request, authToken }) => {
  const startTime = Date.now();
  
  const response = await request.get('/api/users?limit=100', {
    headers: { Authorization: `Bearer ${authToken}` },
  });
  
  const duration = Date.now() - startTime;
  
  expect(response.status()).toBe(200);
  expect(duration).toBeLessThan(500);  // SLA de 500ms
});

Resumo

Padrões que se sustentam em escala:

1. Use fixtures para tokens de autenticação: injete-os, não repita código de login

2. Valide schemas: status 200 com campos errados ainda é um bug

3. Teste CRUD em ordem: criar, ler, atualizar, deletar como ciclo de vida

4. Teste casos de erro: 401, 403, 404, 400, 409 são tão importantes quanto 200

5. Use API para setup/teardown em testes de UI: mais rápido e confiável que setup pela UI

6. Isole os dados: e-mail único por teste, delete depois

A combinação de testes de API (rápidos, completos) e testes de UI (caminhos realistas do usuário) oferece a melhor cobertura com mais confiança.

→ Veja também: Testes de API com o APIRequestContext do Playwright (Sem Postman) | Autenticação em Testes de API: Chaves API, Tokens Bearer, OAuth2, JWT | Testes de API GraphQL com Playwright: Consultas, Mutações e Tratamento de Erros