On this page12 sections
ConceptsIntermediate8-10 min reference

GraphQL Testing

What you need to know to test a GraphQL API — how it differs from REST, what breaks, and what to assert. Pair this with the API Testing Concepts sheet for the REST side.

REST vs GraphQL — what changes for testing

GraphQL exposes a single endpoint (usually POST /graphql) and the client describes exactly what it wants. That inverts several REST testing habits.

AspectRESTGraphQL — testing implication
EndpointsMany URLs, one per resourceOne URL — you test operations, not paths
Status codesErrors via 4xx/5xxUsually 200 OK even on errors — assert on the errors array, not status
Over/under-fetchingCommonClient picks fields — test that field selection is honoured
ShapeServer decidesClient decides — response shape mirrors the query
DiscoveryDocs/OpenAPIIntrospection — the schema is queryable

The single biggest testing trap: GraphQL returns 200 OK for most errors. A failed query comes back as 200 with an errors array in the body. Asserting status === 200 proves nothing — you must assert on body.errors and body.data.

Anatomy of a request

A GraphQL request is a POST with a JSON body carrying query, optional variables, and optional operationName.

curl -X POST https://api.example.com/graphql \
  -H "Content-Type: application/json" \
  -d '{
    "query": "query GetUser($id: ID!) { user(id: $id) { name email } }",
    "variables": { "id": "42" }
  }'
{
  "data": {
    "user": { "name": "Alice", "email": "a@x.com" }
  }
}

Always pass dynamic values through variables, not by string-interpolating them into the query — the same discipline as parameterised SQL, and for the same injection reasons.

Operation types

OperationPurposeTest focus
queryRead dataField selection, nesting, nullability, pagination
mutationWrite dataSide effects, return payload, error handling, idempotency
subscriptionReal-time stream (WebSocket)Connection, message delivery, teardown

Mutations are where state changes, so test both the returned payload and the persisted result with a follow-up query — a mutation can return a success payload while the write silently fails downstream.

Querying fields and nesting

query {
  order(id: "1001") {
    total
    customer {
      name
      address { city country }
    }
    items {
      quantity
      product { name price }
    }
  }
}

Test that you get back exactly the fields you asked for and nothing more. Requesting a field that doesn't exist should produce a validation error before execution.

Variables and the schema

query GetOrders($status: OrderStatus!, $limit: Int = 10) {
  orders(status: $status, first: $limit) {
    id
    total
  }
}

OrderStatus! (non-null) and Int are schema types. The ! means required — omitting a non-null variable is a validation error, which is a cheap, high-value negative test. Default values (= 10) should be exercised by omitting the variable.

The errors array — the thing you actually assert on

{
  "data": { "user": null },
  "errors": [
    {
      "message": "User not found",
      "path": ["user"],
      "extensions": { "code": "NOT_FOUND" }
    }
  ]
}

Key assertions for a GraphQL response:

  • data is present and shaped like the query (for the success case)
  • errors is absent or empty (success) — or present with the expected code (negative case)
  • Partial success is real: a response can carry both data (some fields resolved) and errors (some fields failed). Don't assume one excludes the other.
  • Lean on extensions.code for stable error assertions — message strings change, codes shouldn't.

Schema and introspection

The schema is queryable at runtime via introspection — useful for tests and tooling, risky in production.

query {
  __schema {
    types { name kind }
  }
  __type(name: "User") {
    fields { name type { name } }
  }
}

Test-relevant points: use introspection (or a committed schema SDL) to detect breaking schema changes — a removed field or a field made non-null can break existing clients. Note that introspection is often disabled in production as hardening, so don't write production smoke tests that depend on it.

The N+1 problem

The classic GraphQL performance bug. A query for a list where each item resolves a nested field can fire one database query per item — 1 query for 100 orders, then 100 more for each order's customer.

query {
  orders(first: 100) {
    id
    customer { name }   # naive resolver: 1 DB hit PER order = N+1
  }
}

It usually isn't visible in functional tests (the response is correct) — it surfaces as latency under load. Test for it by asserting on resolver/DB call counts, or watch query timings as list size grows. The fix is server-side (DataLoader-style batching), but QA should catch it.

Authentication

Auth travels in headers, same as REST — the operation itself is in the body.

curl -X POST https://api.example.com/graphql \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d '{ "query": "{ me { id role } }" }'

GraphQL-specific authz testing: because a single query can traverse deep into the graph, test that field-level and object-level authorisation hold through nested fields — a user who can read an order may not be allowed to read its customer.paymentMethod, even in the same query.

Common GraphQL bugs to test for

BugWhat to assert
Errors returned as 200 with no errors arrayFailed operations populate errors with a code
Nested authorisation bypassDeep fields enforce the same authz as top-level
N+1 under loadResolver/DB call count doesn't scale with list size
Over-fetching ignoredRequesting fewer fields returns fewer fields
Non-null field returns nullSchema non-null (!) is honoured or errors correctly
Introspection enabled in prodProduction hardening disables __schema
Unbounded query depth/complexityDeeply nested or expensive queries are rejected/limited

Tooling

ToolUse
Insomnia, HoppscotchSchema-aware GraphQL clients for exploration
PostmanGraphQL inside a full API platform with collections
GraphQL PlaygroundIn-browser schema IDE (largely succeeded by GraphiQL/Apollo Sandbox)
Code-based (any HTTP client)Automated assertion suites in CI

Quick testing checklist

  • Success: data shaped like the query, no errors
  • Negative: bad input produces errors with a stable extensions.code
  • Status is 200 even on logical errors — assert the body, not the status
  • Non-null variables rejected when omitted; defaults applied when omitted
  • Field selection honoured — fewer requested fields means fewer returned
  • Nested authorisation enforced through deep fields
  • Mutations: verify both the return payload and the persisted state
  • Partial success handled (data and errors together)
  • N+1 watched as list size grows
  • Introspection disabled (or intentionally enabled) per environment