GraphQL devuelve HTTP 200 para las consultas fallidas: los errores aparecen en body.errors junto a un body.data nulo, así que un test que solo verifica el código de estado pasa aunque la consulta no haya devuelto nada. Además, cada request de GraphQL es un POST al mismo endpoint /graphql, lo que significa que la consulta en el cuerpo del request determina qué devuelve el servidor y no hay endpoints separados que testear. Esta guía cubre cómo escribir queries y mutations con el fixture request de Playwright, cómo hacer aserciones correctas sobre la forma de la respuesta y los casos de error, y cómo construir un fixture gql reutilizable que elimine el boilerplate repetido.

En qué se diferencia GraphQL de REST

REST: múltiples endpoints, cada uno devuelve una forma fija.

GET /users/1        → { id, name, email, createdAt, ... }
GET /orders/456     → { id, items, total, status, ... }

GraphQL: un solo endpoint (/graphql), siempre POST, tú especificas exactamente qué quieres:

query {
  user(id: "1") {
    name
    email
  }
  order(id: "456") {
    total
    status
  }
}

La consulta va en el cuerpo del request como JSON. La forma de la respuesta coincide exactamente con tu consulta: sin campos extra, sin requests adicionales.

Para el testing esto implica que cada test es un POST a la misma URL, que la forma de la consulta es parte del test (si pides un campo que no existe, obtienes un error) y que los errores de GraphQL no siempre usan códigos de error HTTP.

Escribir tests de GraphQL en Playwright

Usa request.post() para todas las operaciones de GraphQL:

import { test, expect } from '@playwright/test';

test('fetch de usuario devuelve los datos correctos', async ({ request }) => {
  const response = await request.post('https://api.ejemplo.com/graphql', {
    data: {
      query: `
        query GetUser($id: ID!) {
          user(id: $id) {
            id
            name
            email
          }
        }
      `,
      variables: { id: '1' },
    },
    headers: {
      'Content-Type': 'application/json',
      Authorization: 'Bearer tu-token-aqui',
    },
  });

  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('@');
});

Tres cosas a notar:

1. La consulta usa variables ($id: ID!) en lugar de interpolación de strings. Hazlo siempre para evitar problemas de inyección y para que las consultas sean reutilizables.

2. El header Content-Type debe ser application/json.

3. Verificas body.errors explícitamente, porque GraphQL devuelve 200 incluso cuando hay un error.

El modelo de errores de GraphQL

Esto confunde a todos los que testean GraphQL por primera vez.

REST: un request inválido devuelve 400. Sin autorización devuelve 401. Un recurso inexistente devuelve 404.

GraphQL: casi todo devuelve 200. Los errores llegan en el cuerpo de la respuesta junto a (o en lugar de) los datos:

{
  "data": null,
  "errors": [
    {
      "message": "User not found",
      "locations": [{ "line": 2, "column": 3 }],
      "path": ["user"]
    }
  ]
}

Las aserciones deben verificar el cuerpo de la respuesta, no solo el estado HTTP:

// Mal: pasa aunque GraphQL devuelva un error
expect(response.status()).toBe(200);

// Bien: verifica tanto el estado HTTP como el campo de error de GraphQL
expect(response.status()).toBe(200);
const body = await response.json();
expect(body.errors).toBeUndefined();
expect(body.data).not.toBeNull();

Algunos servidores GraphQL sí devuelven 400 para queries mal formadas y 401 para fallos de autenticación, pero no lo des por sentado. Verifica siempre body.errors.

Testear mutations

Las mutations (operaciones de creación, actualización o eliminación) siguen el mismo patrón:

test('mutation de creación de orden', async ({ request }) => {
  const response = await request.post('https://api.ejemplo.com/graphql', {
    data: {
      query: `
        mutation CreateOrder($input: OrderInput!) {
          createOrder(input: $input) {
            id
            status
            total
          }
        }
      `,
      variables: {
        input: {
          productId: 'prod-123',
          quantity: 2,
          shippingAddress: 'Av. Corrientes 1234',
        },
      },
    },
    headers: {
      'Content-Type': 'application/json',
      Authorization: 'Bearer tu-token-aqui',
    },
  });

  const body = await response.json();
  expect(body.errors).toBeUndefined();
  expect(body.data.createOrder.status).toBe('PENDING');
  expect(body.data.createOrder.id).toBeTruthy();
});

Construir un fixture reutilizable para GraphQL

Copiar los headers y el POST en cada test genera ruido. Extráelo a un 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);
  },
});

Con este fixture los tests quedan limpios:

import { test } from '../fixtures/graphql';
import { expect } from '@playwright/test';

test('fetch de usuario', async ({ gql }) => {
  const data = await gql(`
    query { user(id: "1") { name email } }
  `);

  expect(data.user.name).toBe('Alice');
});

Testear casos de error

Verificá que tu API GraphQL devuelve los errores apropiados para entradas inválidas:

test('devuelve error para un usuario inexistente', async ({ request }) => {
  const response = await request.post('https://api.ejemplo.com/graphql', {
    data: {
      query: `query { user(id: "id-que-no-existe") { name } }`,
    },
    headers: { 'Content-Type': 'application/json' },
  });

  const body = await response.json();
  expect(body.errors).toBeDefined();
  expect(body.errors[0].message).toContain('not found');
});

test('devuelve error de autenticación sin token', async ({ request }) => {
  const response = await request.post('https://api.ejemplo.com/graphql', {
    data: { query: `query { user(id: "1") { name } }` },
    headers: { 'Content-Type': 'application/json' },
  });

  const body = await response.json();
  expect(body.errors).toBeDefined();
  expect(body.errors[0].message).toMatch(/unauthorized|unauthenticated/i);
});

Validación de schema

Para un testing más profundo de GraphQL, puedes validar las respuestas contra el schema usando graphql-tag e introspección. Esto detecta automáticamente incompatibilidades de tipos y cambios en los campos, algo valioso en escenarios de contract testing donde el schema es el contrato.

Es territorio avanzado. La mayoría de los equipos empieza con el patrón request/response de arriba y agrega validación de schema más tarde, cuando la API se ha estabilizado.

→ See also: Pruebas de API con el APIRequestContext de Playwright (Sin Postman) | Testing de API Avanzado con Playwright: Patrones para Proyectos Reales | Pruebas de Contrato con Pact: Deja de Romper APIs entre Equipos