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