Тестирование GraphQL
Updated Jul 2026
GraphQL использует единственный эндпоинт (POST /graphql) с языком запросов в теле запроса. В отличие от REST, где сервер решает, какие данные возвращать, в GraphQL клиент указывает, какие именно поля ему нужны. Эта гибкость создаёт уникальные вызовы для тестирования.
Чем GraphQL отличается от REST
| Аспект | REST | GraphQL |
|---|---|---|
| Эндпоинты | Множество (/users, /orders) |
Единственный (/graphql) |
| Форма ответа | Определяется сервером | Определяется клиентом (через запрос) |
| Избыточная выборка | Часто (получаете все поля) | Устранена (запрашиваете только нужные поля) |
| Недостаточная выборка | Часто (нужно несколько запросов) | Устранена (вложенные запросы) |
| HTTP-коды состояния | Используются для всех ошибок | Возвращает 200 для большинства ошибок |
| Обработка ошибок | Код состояния + тело | Всегда проверяйте массив errors в ответе |
Базовое тестирование запросов
def test_graphql_user_query(api, base_url):
query = """
query GetUser($id: ID!) {
user(id: $id) {
name
email
posts {
title
publishedAt
}
}
}
"""
r = requests.post(f"{base_url}/graphql",
json={"query": query, "variables": {"id": "123"}},
headers=api.headers)
assert r.status_code == 200
data = r.json()
# GraphQL returns 200 even for errors — always check the errors array
assert "errors" not in data, f"GraphQL errors: {data.get('errors')}"
assert data["data"]["user"]["name"] == "Alice"
assert isinstance(data["data"]["user"]["posts"], list)
Тестирование мутаций
def test_graphql_create_user(api, base_url):
mutation = """
mutation CreateUser($input: CreateUserInput!) {
createUser(input: $input) {
id
name
email
}
}
"""
variables = {
"input": {
"name": "New User",
"email": "newuser@test.com"
}
}
r = requests.post(f"{base_url}/graphql",
json={"query": mutation, "variables": variables},
headers=api.headers)
data = r.json()
assert "errors" not in data
user = data["data"]["createUser"]
assert user["name"] == "New User"
assert user["id"] is not None
Обработка ошибок в GraphQL
GraphQL возвращает HTTP 200 даже для многих типов ошибок. Ошибки находятся в теле ответа:
def test_graphql_validation_error(api, base_url):
"""Invalid query syntax should return errors array."""
r = requests.post(f"{base_url}/graphql",
json={"query": "{ invalid syntax here }"},
headers=api.headers)
data = r.json()
assert "errors" in data
assert len(data["errors"]) > 0
assert "message" in data["errors"][0]
def test_graphql_not_found(api, base_url):
"""Querying a non-existent resource."""
query = """
query { user(id: "nonexistent") { name, email } }
"""
r = requests.post(f"{base_url}/graphql",
json={"query": query},
headers=api.headers)
data = r.json()
# Implementation-dependent: either errors array or null data
assert data["data"]["user"] is None or "errors" in data
def test_graphql_partial_errors(api, base_url):
"""GraphQL can return partial data with errors."""
query = """
query {
user(id: "123") { name }
nonExistentField { data }
}
"""
r = requests.post(f"{base_url}/graphql",
json={"query": query},
headers=api.headers)
data = r.json()
# May have both data and errors
if "errors" in data:
for error in data["errors"]:
assert "message" in error
Тестирование безопасности
Интроспекция
Интроспекция GraphQL позволяет клиентам запрашивать всю схему — полезно при разработке, но представляет риск безопасности в продакшене.
def test_introspection_disabled_in_production(prod_url):
"""Introspection should be disabled in production."""
query = """
query {
__schema {
types { name }
}
}
"""
r = requests.post(f"{prod_url}/graphql", json={"query": query})
data = r.json()
# Should either error or return no data
assert "errors" in data or data.get("data", {}).get("__schema") is None
Ограничение глубины запросов
Глубоко вложенные запросы могут использоваться для атак типа «отказ в обслуживании»:
def test_query_depth_limit(api, base_url):
"""Deeply nested queries should be rejected."""
# Build a deeply nested query
query = "{ user(id: \"1\") { " + \
"friends { " * 20 + \
"name" + \
" }" * 20 + \
" } }"
r = requests.post(f"{base_url}/graphql",
json={"query": query},
headers=api.headers)
data = r.json()
assert "errors" in data # Should be rejected due to depth limit
def test_query_complexity_limit(api, base_url):
"""Queries requesting too many resources should be limited."""
query = """
query {
users(first: 1000) {
edges {
node {
name
posts(first: 1000) {
edges {
node {
title
comments(first: 1000) {
edges { node { body } }
}
}
}
}
}
}
}
}
"""
r = requests.post(f"{base_url}/graphql",
json={"query": query},
headers=api.headers)
data = r.json()
assert "errors" in data # Should be rejected due to complexity
Авторизация в GraphQL
def test_graphql_unauthorized_field(viewer_api, base_url):
"""Viewer should not be able to query admin-only fields."""
query = """
query {
user(id: "123") {
name
email
internalNotes # admin-only field
}
}
"""
r = requests.post(f"{base_url}/graphql",
json={"query": query},
headers=viewer_api.headers)
data = r.json()
# Either errors or null for the restricted field
if "errors" not in data:
assert data["data"]["user"]["internalNotes"] is None
Практическое упражнение
- Напишите тесты для GraphQL-запроса: валидный запрос, запрос с переменными, запрос несуществующего ресурса
- Напишите тест мутации с валидацией ввода (отсутствующие обязательные поля)
- Проверьте, что интроспекция отключена (или что вы знаете, должна ли она быть включена/отключена)
- Протестируйте ограничение глубины запросов с глубоко вложенным запросом
- Протестируйте авторизацию: проверьте, что разные роли видят разные поля
Ключевые выводы
- GraphQL возвращает HTTP 200 даже для ошибок — всегда проверяйте массив
errors - Тестируйте как запросы, так и мутации с валидными и невалидными входными данными
- Интроспекция должна быть отключена в продакшене (риск безопасности)
- Ограничения глубины и сложности запросов должны быть принудительно применены (предотвращение DoS)
- Авторизация должна тестироваться на уровне полей, а не только на уровне запросов
- Частичные ошибки валидны в GraphQL — ответ может содержать одновременно
dataиerrors