GraphQL возвращает HTTP 200 при неудачных запросах: ошибки попадают в body.errors вместе с null в body.data, поэтому тест проверяющий только статус-код пройдёт даже когда запрос ничего не вернул. Каждый GraphQL-запрос: POST к единственному эндпоинту /graphql, а значит именно тело запроса определяет что получишь в ответ, и отдельных эндпоинтов для тестирования нет. В этом гайде: написание запросов и мутаций через фикстуру request в Playwright, корректные проверки формата ответа и ошибок, а также создание переиспользуемой фикстуры gql которая убирает повторяющийся бойлерплейт.

Чем GraphQL отличается от REST

REST: множество эндпоинтов, каждый возвращает фиксированную форму.

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

GraphQL: один эндпоинт (/graphql), всегда POST, ты сам указываешь что хочешь получить:

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

Запрос передаётся в теле как JSON. Форма ответа точно совпадает с запросом: никаких лишних полей, никаких отдельных запросов.

Для тестирования это означает: каждый тест делает POST на один и тот же URL, форма запроса сама часть теста (попросишь несуществующее поле и получишь ошибку), а GraphQL-ошибки не всегда используют HTTP-коды ошибок (об этом ниже).

Написание GraphQL-тестов в Playwright

Для всех GraphQL-операций используешь request.post():

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

Три вещи стоит заметить. Запрос использует переменные ($id: ID!) вместо интерполяции строк: так избегаешь инъекций и запросы остаются переиспользуемыми. Заголовок Content-Type обязан быть application/json. И всегда явно проверяй body.errors, потому что GraphQL возвращает 200 даже при ошибке.

Модель ошибок GraphQL

Это ставит в тупик всех кто тестирует GraphQL впервые.

REST: плохой запрос возвращает 400, неавторизованный 401, ресурс не найден 404.

GraphQL: почти всё возвращает 200. Ошибки приходят в теле ответа вместе с данными (или вместо них):

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

Проверки должны смотреть в тело ответа, а не только на HTTP-статус:

// Неправильно: проходит даже когда GraphQL вернул ошибку
expect(response.status()).toBe(200);

// Правильно: проверяет и HTTP-статус и поле GraphQL-ошибок
expect(response.status()).toBe(200);
const body = await response.json();
expect(body.errors).toBeUndefined();
expect(body.data).not.toBeNull();

Некоторые GraphQL-серверы возвращают 400 для некорректных запросов и 401 для ошибок авторизации, но полагаться на это не стоит. Всегда проверяй body.errors.

Тестирование мутаций

Мутации (создание, обновление, удаление) следуют тому же паттерну:

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

Переиспользуемая фикстура GraphQL-клиента

Копировать заголовки и POST-вызов в каждый тест шумно. Выносишь в фикстуру:

// 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);
  },
});

Тесты становятся чистыми:

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

Тестирование ошибочных сценариев

Проверяешь что GraphQL API возвращает правильные ошибки при некорректных входных данных:

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' },
    // Заголовок Authorization отсутствует
  });

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

Валидация схемы

Для глубокого тестирования GraphQL: валидация ответов по схеме через graphql-tag и интроспекцию схемы. Автоматически ловит несовпадения типов и изменения полей, полезно в контрактном тестировании где схема служит контрактом.

Это продвинутая территория: большинство команд начинают с паттерна запрос/ответ выше и добавляют валидацию схемы позже когда API стабилизировалось.

→ See also: API-тестирование с Playwright APIRequestContext (без Postman) | Продвинутое API тестирование с Playwright: паттерны для реальных проектов | Контрактное тестирование с Pact: прекратите ломать API между командами