Тестирование GraphQL API: как проверять запросы, мутации и подписки вручную и в автотестах

Тестирование GraphQL API: как проверять запросы, мутации и подписки вручную и в автотестах

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 с полем queryvariables, если нужны)
  • Авторизация: 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 - подробнее ниже).

Тестирование мутаций: побочные эффекты и состояние системы

Мутации изменяют данные - создают, обновляют, удаляют записи. Здесь ошибки стоят дороже, потому что они меняют состояние системы.

Что проверять при работе с мутациями:

  • Результат операции. Мутация должна возвращать изменённый объект или подтверждение. Проверьте, что возвращённые данные соответствуют тому, что было передано на вход.
  • Идемпотентность. Повторный вызов одной мутации с теми же данными - что происходит? Создаётся дубликат или возвращается существующая запись?
  • Валидация входных данных. Передайте невалидные данные: пустые обязательные поля, неверный формат 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: Добавьте в чеклист релиза отдельный пункт - проверку запросов, которые пересекают границы сервисов. Именно такие запросы чаще всего ломаются при обновлении одного из субграфов, потому что их сложнее покрыть изолированными тестами каждого сервиса.