Modern QA2026Тестирование GraphQL
Join

Course14 API Testing Fundamentals

Foundations · Chapter 14

Тестирование 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

Практическое упражнение

  1. Напишите тесты для GraphQL-запроса: валидный запрос, запрос с переменными, запрос несуществующего ресурса
  2. Напишите тест мутации с валидацией ввода (отсутствующие обязательные поля)
  3. Проверьте, что интроспекция отключена (или что вы знаете, должна ли она быть включена/отключена)
  4. Протестируйте ограничение глубины запросов с глубоко вложенным запросом
  5. Протестируйте авторизацию: проверьте, что разные роли видят разные поля

Ключевые выводы

  • GraphQL возвращает HTTP 200 даже для ошибок — всегда проверяйте массив errors
  • Тестируйте как запросы, так и мутации с валидными и невалидными входными данными
  • Интроспекция должна быть отключена в продакшене (риск безопасности)
  • Ограничения глубины и сложности запросов должны быть принудительно применены (предотвращение DoS)
  • Авторизация должна тестироваться на уровне полей, а не только на уровне запросов
  • Частичные ошибки валидны в GraphQL — ответ может содержать одновременно data и errors