O GraphQL retorna HTTP 200 para queries com falha: os erros ficam em body.errors junto com um body.data nulo. Um teste que verifica apenas o status code passa mesmo quando a query não retornou nada. Toda requisição GraphQL também é um POST para um único endpoint /graphql. A query no corpo da requisição determina o que você recebe de volta, e não há endpoints separados para testar.
Como o GraphQL difere do REST
REST: múltiplos endpoints, cada um retornando um formato fixo.
GET /users/1 → { id, name, email, createdAt, ... }
GET /orders/456 → { id, items, total, status, ... }GraphQL: um endpoint (/graphql), sempre POST, você especifica exatamente o que quer:
query {
user(id: "1") {
name
email
}
order(id: "456") {
total
status
}
}A query vai no corpo da requisição como JSON. O formato da resposta corresponde exatamente à sua query: sem campos extras, sem requisições separadas.
Para os testes, isso significa:
- Todo teste é um POST para a mesma URL
- O formato da query é parte do teste: se você pede um campo que não existe, recebe um erro
- Os erros do GraphQL nem sempre usam códigos de erro HTTP (mais sobre isso abaixo)
Escrevendo testes GraphQL no Playwright
Use request.post() para todas as operações GraphQL:
import { test, expect } from '@playwright/test';
test('fetch user returns correct data', async ({ request }) => {
const response = await request.post('https://api.example.com/graphql', {
data: {
query: `
query GetUser($id: ID!) {
user(id: $id) {
id
name
email
}
}
`,
variables: { id: '1' },
},
headers: {
'Content-Type': 'application/json',
Authorization: 'Bearer your-token-here',
},
});
expect(response.status()).toBe(200);
const body = await response.json();
expect(body.errors).toBeUndefined();
expect(body.data.user.name).toBe('Alice');
expect(body.data.user.email).toContain('@');
});Três coisas a observar:
1. A query usa variáveis ($id: ID!) em vez de interpolação de string. Sempre faça isso para evitar problemas de injeção e manter as queries reutilizáveis.
2. O header Content-Type deve ser application/json.
3. Você verifica body.errors explicitamente, porque o GraphQL retorna 200 mesmo quando há um erro.
O modelo de erros do GraphQL
Isso pega todo mundo testando GraphQL pela primeira vez.
REST: uma requisição inválida retorna 400. Uma não autorizada retorna 401. Um recurso ausente retorna 404.
GraphQL: quase tudo retorna 200. Os erros voltam no corpo da resposta junto com (ou no lugar de) os dados:
{
"data": null,
"errors": [
{
"message": "User not found",
"locations": [{ "line": 2, "column": 3 }],
"path": ["user"]
}
]
}Isso significa que suas assertions devem verificar o corpo da resposta, não apenas o status HTTP:
// Errado — passa mesmo quando o GraphQL retorna um erro
expect(response.status()).toBe(200);
// Correto — verifica tanto o status HTTP quanto o campo de erro do GraphQL
expect(response.status()).toBe(200);
const body = await response.json();
expect(body.errors).toBeUndefined();
expect(body.data).not.toBeNull();Alguns servidores GraphQL retornam 400 para queries malformadas e 401 para falhas de autenticação, mas não dependa disso. Sempre verifique body.errors.
Testando mutations
Mutations (operações de criar, atualizar, deletar) seguem o mesmo padrão:
test('create order mutation', async ({ request }) => {
const response = await request.post('https://api.example.com/graphql', {
data: {
query: `
mutation CreateOrder($input: OrderInput!) {
createOrder(input: $input) {
id
status
total
}
}
`,
variables: {
input: {
productId: 'prod-123',
quantity: 2,
shippingAddress: '123 Main St',
},
},
},
headers: {
'Content-Type': 'application/json',
Authorization: 'Bearer your-token-here',
},
});
const body = await response.json();
expect(body.errors).toBeUndefined();
expect(body.data.createOrder.status).toBe('PENDING');
expect(body.data.createOrder.id).toBeTruthy();
});Construindo uma fixture de cliente GraphQL reutilizável
Copiar e colar os headers e a chamada POST em cada teste é verboso. Extraia para uma fixture:
// fixtures/graphql.ts
import { test as base, APIRequestContext } from '@playwright/test';
type GraphQLFixtures = {
gql: (query: string, variables?: Record<string, unknown>) => Promise<Record<string, unknown>>;
};
export const test = base.extend<GraphQLFixtures>({
gql: async ({ request }, use) => {
const gql = async (query: string, variables: Record<string, unknown> = {}) => {
const response = await request.post(process.env.GRAPHQL_URL!, {
data: { query, variables },
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${process.env.API_TOKEN}`,
},
});
const body = await response.json();
if (body.errors) {
throw new Error(`GraphQL error: ${JSON.stringify(body.errors)}`);
}
return body.data;
};
await use(gql);
},
});Agora os testes ficam limpos:
import { test } from '../fixtures/graphql';
import { expect } from '@playwright/test';
test('fetch user', async ({ gql }) => {
const data = await gql(`
query { user(id: "1") { name email } }
`);
expect(data.user.name).toBe('Alice');
});Testando casos de erro
Verifique que a API GraphQL retorna erros apropriados para entradas inválidas:
test('returns error for non-existent user', async ({ request }) => {
const response = await request.post('https://api.example.com/graphql', {
data: {
query: `query { user(id: "non-existent-id") { name } }`,
},
headers: { 'Content-Type': 'application/json' },
});
const body = await response.json();
expect(body.errors).toBeDefined();
expect(body.errors[0].message).toContain('not found');
});
test('returns auth error without token', async ({ request }) => {
const response = await request.post('https://api.example.com/graphql', {
data: { query: `query { user(id: "1") { name } }` },
headers: { 'Content-Type': 'application/json' },
// Sem header Authorization
});
const body = await response.json();
expect(body.errors).toBeDefined();
expect(body.errors[0].message).toMatch(/unauthorized|unauthenticated/i);
});Validação de schema
Para testes GraphQL mais profundos, valide as respostas contra o schema usando graphql-tag e introspecção de schema. Isso detecta incompatibilidades de tipo e mudanças de campo automaticamente, útil em cenários de contract testing onde o schema é o contrato.
É território avançado: a maioria dos times começa com o padrão de requisição/resposta acima e adiciona validação de schema depois que a API se estabilizou.
→ Veja também: Testes de API com o APIRequestContext do Playwright (Sem Postman) | Testes de API Avançados com Playwright: Padrões para Projetos Reais | Testes de Contrato com Pact: Pare de Quebrar APIs Entre Equipes