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 между командами