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