REST-тестировщик, впервые открывший GraphQL-эндпоинт, обычно удивляется: один URL, метод POST, тело запроса в виде строки с фигурными скобками - и никаких привычных путей вроде /users/123. Схема, типы, резолверы, вложенные поля - всё это делает GraphQL принципиально другим протоколом, а не просто «ещё одним API».
Для QA это означает новый набор техник. Часть привычных проверок здесь не работает, зато появляются специфические классы проблем: избыточная выборка данных, цепочки N+1 запросов к базе, уязвимости через introspection, сломанная типизация в мутациях. Если подходить к GraphQL с REST-мышлением, большинство этих багов пройдут мимо.
В этой статье разберём, как устроена схема и что из неё можно извлечь, как проверять запросы и мутации в Postman и Insomnia, где прячутся типичные дефекты, как тестировать подписки и что стоит покрыть автотестами.
Коротко:
- GraphQL работает через один эндпоинт и POST-запросы - привычные HTTP-методы и пути здесь не применимы.
- Introspection позволяет получить полную схему прямо из API - это главный инструмент исследования на старте.
- Мутации меняют данные, запросы читают - тестировать их нужно по-разному, особенно в части побочных эффектов.
- Проблема N+1 и overfetching не видны в одном запросе - их нужно искать целенаправленно.
- Подписки работают через WebSocket и требуют отдельного подхода к проверке.
- Автотесты на GraphQL проще всего строить через прямые HTTP-запросы с телом в виде строки запроса.
Как устроен GraphQL: что важно знать QA до первого запроса
GraphQL - это язык запросов к API, где клиент сам описывает, какие поля ему нужны. Сервер отвечает ровно тем, что запросили, не больше и не меньше (в идеале). Всё взаимодействие идёт через один эндпоинт, обычно /graphql, методом POST.
Тело каждого запроса содержит три возможных ключа:
query- строка с операцией (запрос или мутация)variables- объект с переменными, если они естьoperationName- имя операции, если в теле их несколько
HTTP-статус почти всегда возвращается как 200 OK, даже если в ответе есть ошибки. Это принципиальное отличие от REST: ошибки GraphQL живут внутри тела ответа в поле errors, а не в статус-коде. Тестировщик, который проверяет только статус, пропустит половину дефектов.
Схема (schema) описывает все доступные типы, поля, аргументы и операции. Она строго типизирована: каждое поле имеет тип, обязательность и возможные значения. Нарушение контракта схемы - это уже баг, даже если данные визуально выглядят нормально.
Introspection: как исследовать схему до написания первого теста
Introspection - встроенный механизм GraphQL, позволяющий запросить у сервера описание всей схемы. Это как Swagger, только встроенный в сам протокол.
Базовый introspection-запрос выглядит так:
{
__schema {
types {
name
kind
fields {
name
type {
name
kind
}
}
}
}
}Его можно отправить прямо в Postman или Insomnia как обычный POST-запрос с телом {"query": "{ __schema { types { name } } }"}. Большинство GraphQL-клиентов (GraphiQL, Apollo Sandbox, Altair) делают это автоматически при подключении.
Что проверять через introspection в рамках QA:
- Все ли операции задокументированы в схеме
- Нет ли лишних полей, которые не должны быть публичными
- Соответствуют ли типы полей тому, что реально возвращает сервер
- Включён ли introspection на продакшне (это риск безопасности - см. ниже)
Важно: Включённый introspection на продакшн-окружении позволяет любому пользователю получить полную карту API: все типы, поля, аргументы и операции. Это облегчает разведку перед атакой. Проверьте, закрыт ли introspection для неавторизованных запросов или отключён на prod.
Ручная проверка в Postman и Insomnia
Оба инструмента поддерживают GraphQL нативно. В Postman нужно выбрать тип запроса GraphQL - тогда появится отдельное поле для query и variables. Insomnia работает аналогично и умеет подтягивать схему автоматически через introspection.
Базовая настройка для любого GraphQL-эндпоинта:
- Метод: POST
- URL: адрес GraphQL-эндпоинта
- Content-Type:
application/json - Тело: JSON с полем
query(иvariables, если нужны) - Авторизация: Bearer-токен или cookie - зависит от реализации
Пример простого запроса в теле:
{
"query": "query GetUser($id: ID!) { user(id: $id) { id name email } }",
"variables": { "id": "42" }
}В Postman удобно сохранять такие запросы в коллекцию и переиспользовать переменные окружения для токенов и базовых URL. Это особенно важно, когда нужно проверять одни и те же операции на dev, staging и prod.
Тестирование запросов (query): что проверять
Query - операции чтения. Они не должны менять состояние системы, но это не значит, что их легко проверять.
Ключевые проверки для запросов:
- Запрос только нужных полей. Попробуйте запросить подмножество полей - ответ должен содержать именно их, без лишнего. Если сервер возвращает поля, которые не запрашивались, это overfetching и нарушение контракта.
- Вложенные объекты. GraphQL позволяет запрашивать связанные сущности в одном запросе. Проверьте, что вложенные данные корректны и соответствуют родительской записи.
- Аргументы и фильтры. Передайте граничные значения: пустую строку, null, очень длинную строку, отрицательное число. Посмотрите, как сервер обрабатывает невалидные аргументы.
- Пагинация. Проверьте поведение при первой странице, последней, пустом результате и запросе за пределами диапазона.
- Несуществующие поля. Запросите поле, которого нет в схеме - сервер должен вернуть ошибку валидации, а не молча игнорировать.
Пример сценария: Запрашиваем список заказов пользователя с вложенными товарами. Проверяем, что при запросе 100 заказов каждый из них не вызывает отдельный запрос к базе данных (это и есть N+1 - подробнее ниже).
Тестирование мутаций: побочные эффекты и состояние системы
Вакансии для QA-инженеров
Мутации изменяют данные - создают, обновляют, удаляют записи. Здесь ошибки стоят дороже, потому что они меняют состояние системы.
Что проверять при работе с мутациями:
- Результат операции. Мутация должна возвращать изменённый объект или подтверждение. Проверьте, что возвращённые данные соответствуют тому, что было передано на вход.
- Идемпотентность. Повторный вызов одной мутации с теми же данными - что происходит? Создаётся дубликат или возвращается существующая запись?
- Валидация входных данных. Передайте невалидные данные: пустые обязательные поля, неверный формат email, строку вместо числа. Ответ должен содержать понятную ошибку в поле
errors. - Авторизация. Попробуйте выполнить мутацию без токена, с чужим токеном, с токеном без нужных прав. Сервер должен отказывать, а не выполнять операцию.
- Побочные эффекты. После мутации проверьте смежные данные: если создали заказ, появился ли он в списке заказов? Обновился ли счётчик?
| Сценарий мутации | Что проверять | Типичная ошибка |
|---|---|---|
| Создание записи | Возвращаемый ID, обязательные поля, дубликаты | Нет валидации уникальности |
| Обновление записи | Только изменённые поля, версионирование | Перезапись несвязанных полей |
| Удаление записи | Ответ при повторном удалении, каскадные эффекты | Мягкое удаление без пометки |
| Мутация без прав | Код ошибки в errors, отсутствие изменений | Частичное выполнение до проверки прав |
Проблема N+1: как её обнаружить при тестировании
N+1 - одна из самых распространённых проблем производительности в GraphQL. Суть: при запросе списка из N объектов сервер делает N дополнительных запросов к базе данных для загрузки связанных данных каждого объекта, вместо одного батч-запроса.
Пример: запрашиваем 50 постов с именами авторов. Сервер получает 50 постов одним запросом, а потом делает ещё 50 отдельных запросов к таблице пользователей - по одному на каждый пост. Итого 51 запрос вместо 2.
Как обнаружить это в тестировании:
- Включите логирование SQL-запросов на тестовом окружении и посмотрите на количество запросов при загрузке вложенных данных.
- Используйте инструменты профилирования: Apollo Studio показывает трассировку резолверов, Django Debug Toolbar (для Python) - SQL-запросы.
- Сравните время ответа при запросе 1 объекта и 100 объектов с вложенными данными - если разница нелинейная, скорее всего есть N+1.
- Проверьте, используется ли DataLoader или аналогичный механизм батчинга.
N+1 не всегда критичен на малых данных, но в продакшне с реальными объёмами он превращается в серьёзную проблему производительности.
GraphQL подписки: как тестировать real-time операции
Подписки (subscriptions) - механизм получения данных в реальном времени. В отличие от запросов и мутаций, они работают через WebSocket, а не через обычный HTTP-запрос.
Для ручной проверки подписок подходят:
- Altair GraphQL Client - поддерживает подписки нативно, удобный интерфейс
- GraphiQL - встроенный клиент многих GraphQL-серверов
- Postman - поддержка подписок появилась в версии 10+
Что проверять при работе с подписками:
- Установка соединения. Подписка должна успешно подключаться и оставаться активной.
- Получение событий. После выполнения мутации, которая триггерит подписку, клиент должен получить обновление.
- Фильтрация событий. Если подписка принимает аргументы (например,
subscription { orderUpdated(userId:
Автотесты для GraphQL: что покрывать и как строить
Автоматизировать GraphQL-тесты проще, чем кажется. Никакой специальной библиотеки не нужно: любой HTTP-клиент умеет отправлять POST-запросы с JSON-телом. Это работает на Python с requests, на JavaScript с axios или fetch, на Java с RestAssured.
Базовая структура автотеста для запроса:
import requests
url = "https://api.example.com/graphql"
headers = {"Authorization": "Bearer TOKEN", "Content-Type": "application/json"}
payload = {
"query": "query GetUser($id: ID!) { user(id: $id) { id name email } }",
"variables": {"id": "42"}
}
response = requests.post(url, json=payload, headers=headers)
data = response.json()
assert response.status_code == 200
assert "errors" not in data
assert data["data"]["user"]["id"] == "42"Обратите внимание: проверяем и HTTP-статус, и отсутствие поля errors в теле. Только одной из этих проверок недостаточно.
Что стоит покрыть автотестами в первую очередь:
- Все критичные мутации: создание, обновление, удаление ключевых сущностей
- Авторизационные сценарии: запрос без токена, с истёкшим токеном, с токеном без нужных прав
- Контракт схемы: типы возвращаемых полей соответствуют ожидаемым
- Граничные значения аргументов: null, пустая строка, максимальная длина
- Ответ при запросе несуществующей записи: null в data или ошибка в errors
Для более сложных сценариев есть специализированные библиотеки: gql для Python, graphql-request для JavaScript. Они добавляют удобный синтаксис и автодополнение по схеме, но не меняют принципиального подхода.
Типичные ошибки в обработке ошибок GraphQL
GraphQL имеет нестандартную модель ошибок, и именно здесь тестировщики чаще всего упускают дефекты. Сервер возвращает HTTP 200, но в теле ответа одновременно могут быть и данные, и ошибки. Это называется partial success: часть запроса выполнилась, часть нет.
Пример ответа с частичной ошибкой:
{
"data": {
"user": {
"id": "42",
"name": "Иван",
"orders": null
}
},
"errors": [
{
"message": "Недостаточно прав для просмотра заказов",
"path": ["user", "orders"],
"extensions": { "code": "FORBIDDEN" }
}
]
}Что нужно проверять в структуре ошибок:
- Поле
messageсодержит понятное описание, а не внутренний стек-трейс или SQL-запрос. - Поле
pathуказывает на конкретное поле, которое вызвало ошибку. - Поле
extensions.codeсодержит машиночитаемый код ошибки (FORBIDDEN, NOT_FOUND, VALIDATION_ERROR), а не только текст. - При ошибке авторизации сервер не возвращает частичные данные, которые пользователь не должен видеть.
- Технические детали реализации не утекают в сообщения об ошибках на продакшне.
Частая проблема: Разработчики настраивают детальные ошибки для удобства отладки, но забывают отключить их на продакшн-окружении. В результате пользователь видит сообщения вида «column users.internal_score does not exist» или полный стек-трейс. Проверяйте поведение ошибок отдельно на каждом окружении.
Переменные и директивы: что проверять дополнительно
Переменные в GraphQL не просто синтаксический сахар. Они влияют на безопасность и корректность запросов. Если клиент передаёт значения напрямую в строку запроса (интерполяция), а не через variables, это потенциальная уязвимость инъекции и нарушение контракта.
Что стоит проверить при работе с переменными:
- Передача null для обязательной переменной (тип без восклицательного знака и с ним ведут себя по-разному).
- Передача переменной неверного типа: строки вместо числа, массива вместо объекта.
- Очень большой объект в переменных: сервер должен ограничивать размер входных данных.
- Отсутствие переменной при её объявлении в запросе.
Директивы @include и @skip позволяют условно включать или исключать поля. Это тоже требует проверки:
- Поле с
@skip(if: true)не должно появляться в ответе. - Поле с
@include(if: false)не должно вызывать ошибку, если оно обязательное в схеме. - Комбинация обеих директив на одном поле: поведение должно быть предсказуемым.
Сравнение инструментов для ручного тестирования
Выбор клиента влияет на удобство работы и на то, какие проверки вообще доступны без написания кода. Вот краткое сравнение популярных вариантов:
| Инструмент | Introspection | Подписки | Переменные | Коллекции |
|---|---|---|---|---|
| Postman | Да | С версии 10+ | Да | Да |
| Insomnia | Да | Да | Да | Да |
| Altair | Да | Да | Да | Да (платно) |
| GraphiQL | Да | Зависит от сервера | Да | Нет |
| Apollo Sandbox | Да | Да | Да | Нет |
Для большинства задач ручного тестирования достаточно Postman или Insomnia: они уже знакомы команде, поддерживают окружения и переменные, умеют работать со схемой. Altair удобен, если нужно активно работать с подписками или часто переключаться между схемами разных сервисов.
Чеклист перед передачей GraphQL-фичи в релиз
Когда фича на GraphQL готова к проверке, удобно иметь короткий список обязательных проверок. Это не замена полноценному тест-плану, а минимум, без которого не стоит двигаться дальше.
Минимальный чеклист для GraphQL-фичи:
- Схема обновлена и соответствует реальному поведению сервера.
- Все новые поля и аргументы задокументированы в схеме с описаниями.
- Запросы возвращают только запрошенные поля, без лишних данных.
- Мутации валидируют входные данные и возвращают понятные ошибки при невалидных значениях.
- Авторизационные проверки работают: неавторизованный запрос получает ошибку, а не данные.
- Ошибки содержат код и сообщение, но не технические детали реализации.
- Проверено поведение при null, пустых строках и граничных значениях аргументов.
- На продакшн-окружении introspection закрыт или требует авторизации.
- Если есть вложенные данные, проверено отсутствие N+1 через логи или профилировщик.
- Для подписок проверено получение события после соответствующей мутации.
Тестирование производительности GraphQL: на что обращать внимание
GraphQL даёт клиенту большую свободу в формировании запросов, и это создаёт специфические риски производительности, которых нет в REST. Клиент может запросить глубоко вложенную структуру данных в одном запросе, и сервер будет вынужден обходить несколько уровней связей.
Основные сценарии, которые стоит проверять отдельно:
- Глубина вложенности. Запрос вида «пользователь - его заказы - товары в заказе - категории товаров - связанные категории» может вызвать лавинообразный рост нагрузки. Проверьте, есть ли ограничение на максимальную глубину запроса.
- Количество запрашиваемых полей. Запрос всех полей всех типов одновременно должен либо ограничиваться сервером, либо выполняться за разумное время. Если нет ни того ни другого, это риск.
- Сложность запроса (query complexity). Многие GraphQL-серверы поддерживают подсчёт сложности запроса и отклоняют слишком тяжёлые. Проверьте, работает ли этот механизм: отправьте намеренно сложный запрос и посмотрите, вернёт ли сервер ошибку с кодом вроде
QUERY_TOO_COMPLEX. - Пагинация на больших данных. Запрос без ограничения количества записей (без аргументов
firstилиlimit) должен либо возвращать разумное количество по умолчанию, либо требовать явного указания лимита.
Практический сценарий: Отправьте запрос с намеренно большой вложенностью, например 8-10 уровней. Зафиксируйте время ответа и проверьте, есть ли в ответе ошибка ограничения. Если сервер молча выполняет такой запрос за несколько секунд, это повод для разговора с командой разработки о защите от злоупотреблений.
Тестирование кеширования в GraphQL
Кеширование в GraphQL устроено иначе, чем в REST. В REST каждый URL можно кешировать на уровне HTTP-заголовков. В GraphQL все запросы идут на один эндпоинт методом POST, поэтому стандартный HTTP-кеш здесь не работает. Кеширование реализуется на уровне приложения: через DataLoader, Redis, Persisted Queries или CDN с особой конфигурацией.
Что проверять в части кеширования:
- После обновления данных через мутацию повторный запрос должен возвращать актуальные данные, а не устаревшие из кеша.
- Данные одного пользователя не должны попадать в кеш и возвращаться другому пользователю.
- Если используются Persisted Queries, проверьте, что изменение запроса при том же хеше не приводит к выполнению старой версии.
- Заголовки ответа: убедитесь, что чувствительные данные не кешируются на уровне браузера или прокси (заголовок
Cache-Control: no-storeдля приватных данных).
| Уровень кеширования | Что проверять | Риск при ошибке |
|---|---|---|
| DataLoader (батчинг) | Данные актуальны после мутации | Устаревшие данные в ответе |
| Redis / in-memory | Инвалидация после изменений | Пользователь видит чужие данные |
| Persisted Queries | Хеш соответствует актуальному запросу | Выполнение устаревшей операции |
| HTTP-заголовки | Приватные данные не кешируются | Утечка данных через прокси |
Тестирование GraphQL в контексте микросервисов
Многие команды используют GraphQL как единый шлюз над несколькими микросервисами - это называется Federation или Schema Stitching. С точки зрения QA это добавляет новый класс проблем, которые не видны при тестировании каждого сервиса по отдельности.
Специфика проверок в federated-архитектуре:
- Граница сервисов. Запрос, который затрагивает данные из двух разных сервисов, может вернуть частичный результат, если один из сервисов недоступен. Проверьте, как шлюз обрабатывает недоступность одного из источников.
- Согласованность типов. Если один сервис расширяет тип, определённый в другом, проверьте, что поля корректно объединяются и не конфликтуют.
- Трассировка запросов. При ошибке в federated-запросе важно понять, в каком именно сервисе она произошла. Проверьте, содержит ли поле
pathв ошибке достаточно информации для диагностики. - Версионирование схем. При обновлении схемы одного из сервисов проверьте, что общая схема шлюза остаётся консистентной и не ломает существующие запросы.
Если в проекте используется Apollo Federation, удобно проверять схему через rover CLI: он умеет валидировать совместимость субграфов и обнаруживать конфликты до деплоя.
Совет для команд с federation: Добавьте в чеклист релиза отдельный пункт - проверку запросов, которые пересекают границы сервисов. Именно такие запросы чаще всего ломаются при обновлении одного из субграфов, потому что их сложнее покрыть изолированными тестами каждого сервиса.