GraphQL Testing
GraphQL testing verifies the typed query layer: operations, variables, the schema and error handling. One endpoint serves a schema, so tests focus on which fields an operation selects, whether arguments behave, and how the resolver graph resolves nested data.
What is this?
GraphQL testing verifies the typed query layer: operations (queries and mutations), variables, schema and error handling. Instead of many REST endpoints, one endpoint serves a schema, so tests focus on which fields an operation selects, whether arguments behave, and how the resolver graph resolves nested data. Coverage includes both the shape of responses and the schema contract itself.
Why do you need it?
- The schema is the contract: testing it catches removed fields and changed types before clients break.
- Queries fetch nested data in one round trip, so tests verify the whole resolver graph at once.
- Variables isolate the operation from input data, making tests repeatable and data-driven.
- GraphQL error handling is non-standard (200 with an errors array), so it must be tested explicitly.
- Introspection lets tests assert the schema automatically and document what is actually available.
Mental model
A menu with categories and dishes
A GraphQL schema
The menu tells you exactly what can be ordered and how each dish is composed; you pick only what you want and the kitchen sends precisely that. The schema is the menu — introspection reads it, and tests confirm every dish you order arrives as described.
How it works
- Write a query or mutation operation that selects the exact fields you want.
- Send it as an HTTP POST (or via a client like Apollo or graphql-request) with optional variables.
- Assert on data paths returned by the resolver graph and on the errors array when they should occur.
- Validate the schema with introspection or a schema snapshot so type changes fail CI immediately.
- Test arguments and nullability: required args, invalid types, and fields that legitimately return null.
An operation travels through validation, resolvers and the response envelope.
Key commands
curl -s -X POST https://api.example.com/graphql -H 'Content-Type: application/json' -d '{"query":"{ __typename }"}'Send a minimal introspection query to confirm the endpoint is alive.
{"data":{"__typename":"Query"}}curl -s -X POST https://api.example.com/graphql -H 'Content-Type: application/json' -d @query.json | jq .Post an operation from a file and pretty-print the JSON response.
npx graphql-schema-linter schema.graphqlLint the schema for common problems like missing descriptions or casing issues.
npx @graphql-inspector/cli validate schema.graphql new-schema.graphqlDiff two schema versions and fail on breaking changes like removed fields.
npx apollo schema:download schema.graphql --endpoint https://api.example.com/graphqlDownload the live schema for diffing against the codebase's version.
Examples
Run a query with variables and assert the result
const query = [ "query Order($id: ID!) {", " order(id: $id) {", " id", " status", " lineItems { sku quantity }", " }", "}",].join("\n"); const res = await fetch("https://api.example.com/graphql", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ query, variables: { id: "42" } }),});const json = await res.json(); if (json.errors) throw new Error(json.errors[0].message); console.assert(json.data.order.status === "shipped", "order should be shipped");console.assert(json.data.order.lineItems.length === 2, "expected two line items");The operation declares a required ID! variable and selects only the fields the test needs. The POST body carries the query and variables as JSON. GraphQL can return HTTP 200 even when a resolver failed, so the test checks json.errors first, then asserts on the exact field paths in json.data. The join approach keeps the query readable without embedded backticks.
In production
- Schema snapshots and linting run in CI so breaking type changes are caught at pull-request time.
- Mutation tests verify idempotency and side effects that a REST PUT endpoint would otherwise cover.
- Performance tests measure query depth and over-fetching, driving resolver-level fixes.
- Introspection-based tests prove every field in the changelog actually exists and resolves.
Best practices
- Assert on the specific field paths you need rather than the entire response object.
- Use variables for input and keep fixtures out of the query strings.
- Always assert the errors array for failing operations — a 200 can still mean a failed request.
- Snapshot the schema in CI and review diffs like code, with the API team.
- Test nullability explicitly: a field that may be null should be tested both present and absent.
Common mistakes
- Forgetting that GraphQL returns HTTP 200 with an errors array — checking status alone hides failures.
- Requesting top-level fields without checking nested selections or resolver behavior.
- Hard-coding large IDs and values in queries, making tests fragile and unrepeatable.
- Never testing mutations with side effects, so write operations break in production undetected.
Common issues
Cannot query field 'name' on type 'User'The field was renamed, removed, or requires a different selection set.
Introspect the type, update the query to the current field name, and re-run.
Variable '$id' of required type 'ID!' was not providedThe operation declares a required variable that was not passed.
Add the variable to the variables object and ensure the JSON key matches the declared name.
HTTP 200 but result.errors is populatedThe operation ran but a resolver failed or validation errored; GraphQL still returns 200.
Assert on errors in tests and surface resolver exceptions from the server logs.
Debugging tips
- Read the errors array message and locations — they point at the exact field and line in the operation.
- Use an IDE with a GraphQL extension or the explorer UI to validate queries before writing tests.
- Check extensions and context in the response for resolver timing and tracing data.
- Run the operation against the introspection endpoint to confirm the field exists and its type.
Interview checkpoint
Hands-on lab
- Send a query and assert on the returned data paths
- Parameterize an operation with variables
- Trigger an error and assert the errors array
- Validate the schema with introspection or a schema linter
Test Queries, Mutations and the Schema
40 min · intermediate
Quick quiz
Which part of a GraphQL operation is used to pass dynamic input without hard-coding it?
A GraphQL response with an errors array always returns HTTP 400.
Your query selects user { id name } but the response only contains null for name.
Cheat sheet
query { user(id: "42") { id name } }Read datamutation { createOrder(items: [1,2]) { id } }Change statequery ($id: ID!) { ... }Declare variables{"query":"...","variables":{"id":"42"}}POST body shapenpx graphql-schema-linter schema.graphqlLint the schemanpx @graphql-inspector/cli validate a bFail on breaking diffsnpx apollo schema:download s.graphql --endpoint <url>Fetch live schema