- GraphQL returns HTTP 200 for almost everything, including errors — so status-code assertions are nearly useless and tests must read the
errorsarray. - Schema introspection gives you the complete type graph, which makes automated test generation more tractable than for REST.
- The N+1 problem turns a request for 100 records into 101 database round-trips, and is invisible to response assertions.
- Production schemas need depth and complexity limits — without them a single small query can force exponential resolution cost.
How GraphQL testing differs from REST
The differences are structural, and they invalidate most REST testing habits.
| REST | GraphQL | |
|---|---|---|
| Endpoints | Many, one per resource | One, usually /graphql |
| Errors | 4xx / 5xx status codes | HTTP 200 with an errors array |
| Response shape | Fixed by the server | Determined by the client's query |
| Discovery | OpenAPI, if it exists | Introspection, always available |
| Over-fetching | Common | Solved — but replaced by cost problems |
The single most important consequence: a 200 response can be a complete failure. A GraphQL server that cannot resolve a field returns 200 with data: null and a populated errors array. A test asserting only on status code will pass while every field failed to resolve.
Testing with introspection
Introspection is GraphQL's most useful testing affordance. A single query returns the entire schema — every type, field, argument, nullability marker and enum value — which is more machine-readable structure than most REST APIs ever document.
From that graph you can derive test cases mechanically: a query for every field, a mutation for every input type, null probes for every non-nullable field, and invalid-type probes for every argument. This is exactly the work auto-discovery performs before generating a suite.
Note that introspection is often disabled in production, and reasonably so — it hands an attacker a complete map of your API. Test against an environment where it is enabled, then run the generated suite against production.
What to assert on
- The
errorsarray is absent on success. This is the assertion that replaces the status code check. datais not null, and the requested fields are present with the right types.- Partial failures are handled. GraphQL can return partial data alongside errors — some fields resolved, others didn't. Tests should distinguish complete success from partial success.
- Error shape is stable. Consumers parse
message,pathandextensions.code. Changing those is a breaking change just as surely as renaming a data field. - Nullability matches the schema. A field declared non-null that returns null is a server error, and it nullifies the entire parent object.
The N+1 problem
A query that fetches a list and then a related field for each item resolves the list with one query, then fires one query per item. Requesting 100 posts with their authors becomes 1 + 100 = 101 database round-trips.
query {
posts(first: 100) { # 1 query
title
author { name } # + 100 queries, one per post
}
}
This is invisible to response assertions — the data is correct, it just cost a hundred times more than it should. Detecting it requires observing query counts or resolver timing during the test rather than only the response. The standard fix is a batching data loader that collects the author IDs and fetches them in a single query.
Depth and complexity limits
Because clients compose their own queries, a client can nest recursive relationships arbitrarily deep:
query {
user { posts { author { posts { author { posts { ... } } } } } }
}
Each level multiplies the resolution cost. A compact query can force an enormous amount of work — a denial-of-service vector requiring no special access. Production schemas should enforce a maximum query depth and a complexity budget that weights expensive fields more heavily, and your tests should confirm that an over-deep query is actually rejected rather than merely slow.
Testing mutations
Mutations need three assertions, and the third is the one usually skipped:
- The response payload is correct — the mutation returned what it promised.
- Invalid input is rejected in the errors array rather than partially applied.
- The state actually changed. Query the resource afterwards and confirm. Mutations that report success without persisting are a common and easily missed defect.
Mutations also need the same authorization probes as REST: authenticate as one user, attempt to mutate another user's resource, and confirm it is refused. See the security testing guide — object-level authorization flaws are as common in GraphQL as in REST, and the single endpoint makes them easier to overlook.
Test your API without writing the tests
Paste a URL. Flasqo discovers your endpoints, generates the suite and runs it — free, no credit card.
Start testing free