A successful HTTP request can still contain a failed GraphQL field or expose data the caller should never receive.
GraphQL testing checks what happens when a client executes an operation against an API's schema and application logic. A useful test follows the request far enough to catch incorrect variables, failed field resolution, and unauthorized data. Checking only an HTTP status code misses failures represented inside the response body.
Start with operations your product uses. Keep their documents beside assertions about returned values and observable writes. Every product scenario below is hypothetical and describes proposed test design, not an experiment, measured result, or production incident.
Choose the execution boundary for each test
Separate resolver logic from operation execution
Use unit testing for resolver branches that deserve isolated checks, such as mapping a service failure or forwarding the caller's identity. Inject dependencies through context and await asynchronous results. The GraphQL.js resolver testing guide explains direct function tests without constructing a schema. A mock repository can make these checks precise, but it cannot prove that the executable schema wires the resolver correctly.
For integration testing, import the application's executable schema and execute a real document. GraphQL.js supports graphql() with schema, source, variableValues, and contextValue. Assert returned data and errors, as shown in its operation testing guide. Retain real resolvers while replacing selected external services. Use an isolated database where persistence behavior matters, and document which boundaries each test substitutes.
Keep HTTP coverage explicit
Apollo Server's executeOperation runs its operation pipeline without an HTTP request. Supplying contextValue bypasses context initialization, so it cannot demonstrate that a bearer token becomes the correct caller. Apollo's integration testing documentation recommends actual HTTP requests for testing context creation.
Add endpoint tests using the production HTTP integration with controlled dependencies. Send credentials through real headers or cookies and inspect serialized responses. A small set should cover authentication, malformed transport input, and application error formatting. Keep resolver tests when they help locate failures; avoid duplicating every operation at every boundary.
Build a repeatable GraphQL testing workflow

Turn product operations into executable checks
Map operations
Inventory named queries and mutations used by supported clients. Record each operation's variables, caller permissions, and necessary fixtures. Prioritize writes, sensitive fields, and operations with nullable dependencies. Include the actual fragments and conditional selections used by the client so validation sees the complete document.
In an example project management API, map ProjectOverview and RenameProject first. Seed two organizations with separate memberships. Give each worker an isolated fixture namespace, freeze time where relevant, and reset writes between tests. A test should explain its own starting state without depending on another operation having run earlier.
Assert responses
Execute each document through the real schema. For a successful query, check selected values and the absence of errors. For a mutation, also inspect persisted state through an independent repository read. A resolver could construct a convincing response without saving anything.
Add explicit failure expectations before broad snapshots. In the example RenameProject case, an invalid business name should leave the stored project unchanged. Specify whether the application models that failure as payload data or a GraphQL error. Those representations require different assertions and should follow the API's documented policy.
Verify access
Repeat sensitive operations with an owner, a member with limited permissions, and a caller from the other organization. Send sensitive queries and mutations without credentials through HTTP, including on private APIs, and assert rejection with no protected data or durable writes. Separately verify allowed anonymous selections where public access is intentional. Check denied writes against stored state as well as response content.
For the example project API, another organization's caller must not retrieve the project through either its direct identifier or a nested relationship. Assert allowed neighboring fields separately when the policy permits a partial response. Keep fixture identity visible in failure output without logging credentials.
Validate documents and variable values separately
Reject invalid requests before resolver work
GraphQL document validation checks field selections, argument usage, and fragment compatibility against the schema. Invalid documents produce request errors before field execution; see the validation guide. Test an unknown field and an invalid selection shape, then assert errors exist, data is absent, and a write spy or independent database read confirms no side effect.
Variable coercion needs separate cases. For a required variable without a default, test omission and explicit null. Also test incompatible scalar values, unknown enum values, and invalid input object fields. A valid document can still fail when supplied variable values cannot be coerced. Keep transport shape errors, such as variables supplied as an array, in HTTP tests.
Exercise the client's actual selections
Pass variables separately instead of interpolating user input into operation strings. The query documentation covers variable definitions, defaults, aliases, and fragments. Test omitted optional inputs separately from explicit null when your update semantics distinguish keeping a value from clearing it.
In an example project settings API, omitting description might preserve it while null clears it. That is an application rule to test, not a universal GraphQL update convention. Exercise both outcomes through the same mutation document. Include directive branches and concrete types returned through interfaces or unions when clients depend on those response shapes.
Assert errors and null propagation precisely
Distinguish request errors from field failures
GraphQL can return data alongside errors after execution begins. Request errors prevent execution and omit data. The response guide explains these different envelopes. Do not normalize an absent data key and data containing null into one assertion, because they convey different execution outcomes.
In an example query selecting project name and a nullable forecast field, make the forecast service fail. Assert the project name survives, forecast is null, and the error identifies the failed response path. Repeat with an alias and assert the aliased path. For list failures, include the numeric item index. Avoid requiring an ordering between independent errors unless the application establishes one.
Follow nullability through the response tree
A null result for a nonnullable field propagates to the nearest nullable boundary. It can nullify an object, a list, or the whole data value. The GraphQL.js nullability guide describes this execution behavior.
In an example schema where project is nullable and Project.name is String!, returning null for name should make project null. The error path still identifies name. With nullable list elements, a failed nonnullable child can nullify one element; with nonnullable elements, propagation continues through the list boundary. Build fixtures for the exact wrappers in your schema and assert the surviving siblings. Calling the resolver directly cannot verify these response transformations.
Test mutations through durable effects
Define write behavior beyond the payload
GraphQL executes top level mutation fields serially. This does not provide database transactions or automatic rollback, as the mutation documentation explicitly explains. Avoid writing a test that assumes a later failure undoes an earlier successful mutation field.
In an example operation with two rename fields, arrange for the second to fail and verify the first write according to the application's policy. Test transaction guarantees inside the service that owns them. For a retryable create operation, define an application idempotency policy and verify duplicate submissions against durable records. GraphQL itself does not promise deduplication. Run concurrent requests separately when testing uniqueness or race conditions, since serial execution within one operation cannot establish behavior between requests.
Verify HTTP semantics for the deployed server
Assert media types alongside status codes
Send POST requests with Content-Type application/json and an Accept header advertising application/graphql-response+json. If supporting legacy application/json responses, test that negotiation separately. The HTTP serving guide describes the media types, POST support, optional GET queries, and differing error status conventions. GET must not execute mutations.
Assert status, response Content-Type, and body together. Cover malformed JSON, a document validation error, variable coercion failure, and a resolver error producing partial data. A partial response with nonnull data uses a success HTTP status under the documented guidance. Legacy JSON servers may also return success statuses for validation errors. Record the expected behavior for your implementation and media type rather than declaring that GraphQL always returns 200.
Apollo behavior also depends on version and configuration. Its error handling reference documents the Apollo Server 4 variable coercion regression and the corrected default in Server 5. Pin those expectations to the deployed version. Error extension codes are implementation conventions; assert the codes your application publishes without treating Apollo names as language requirements.
Verify authorization across object and field paths
Reach the same data through different relationships
GraphQL's authorization guidance recommends placing permission rules in business logic called by resolvers. A valid identity alone does not establish permission to read every selected field.
In an example organization API, try another organization's project identifier through project(id), a node lookup if present, and a nested member relationship. Query a public project field beside a restricted billing field. Verify the documented denial representation and any null propagation while confirming that allowed data remains available where intended.
Repeat sensitive selections inside fragments and aliases. Check aggregate counts and pagination metadata for disclosures even when individual nodes are filtered. Run two HTTP requests under different identities to detect accidental reuse of request context or cached authorized objects. Keep loaders scoped appropriately and assert that one caller's earlier read cannot authorize a later caller. Denied mutations must leave records unchanged.
Bound pagination and execution cost
Check continuation and resource limits
For APIs using connections, test edges and pageInfo together. The pagination guide describes opaque cursors and continuation metadata. In an example fixed dataset ordered by timestamp and identifier, request consecutive pages and verify the expected sequence without duplicates or missing records. Test empty results and the final page. Define invalid cursor and changed filter behavior explicitly; opaque cursors do not guarantee snapshot consistency during concurrent writes.
Test the maximum allowed page size and one value beyond it. Confirm whether the implementation rejects or caps the request, and check underlying work as well as response length. Fetching every record and slicing afterward still permits expensive work.
The GraphQL security guide discusses depth, breadth, and complexity controls. Depth limits alone cannot bound many shallow aliased fields. In an example policy with a cost budget of 100 units, test a calculated cost of 100 and 101, including variable driven page sizes. These are example policy values, not recommended universal limits. Verify rejection before expensive resolver work and include nested list multipliers. Test aggregate batch limits only if HTTP batching is enabled.
Keep CI evidence reproducible
Validate supported client documents against the candidate schema in CI, then run execution checks and focused HTTP integration tests. Preserve operation name, schema revision, fixture identity, and sanitized failure details. Use deterministic clocks and isolated databases so retries do not conceal shared state problems. Test production error formatting to catch exposed stack traces or internal service details.
Use the contract testing guide for independently released consumer compatibility and the API rate limit testing guide for throttling and recovery coverage. Those checks complement execution assertions. Fail CI when required operations are skipped or fixture setup fails, and give each failure a responsible owner.
Create a BetaRoast feedback round to ask people whether partial results, permission messages, and retry actions make sense in the product. Provide safe accounts and a concrete mission. BetaRoast supplies human feedback; it does not execute GraphQL operations or replace automated API checks.
Frequently asked questions
What should GraphQL API testing cover first?
Start with product operations that write data or expose restricted fields. Execute real documents against the application schema and resolvers, then check variables, returned values, errors, and durable effects.
Can resolver unit tests replace integration tests?
No. Direct calls help test local logic but bypass document validation, field selection, serialization, and null propagation. Keep operation execution tests and focused HTTP checks for those boundaries.
Does a successful HTTP status mean a query succeeded?
No. An executed operation can return partial data and errors with a success status. Check the response body and negotiated media type alongside the server's documented status behavior.
How should query complexity limits be tested?
Use operations at the configured budget and just beyond it. Include aliases, nested lists, and variable driven sizes, then verify expensive work does not start for rejected requests.
Run real operations through the executable schema, assert data and errors together, and verify authorization through every relevant object and field path. Keep HTTP checks separate from in process execution, bound pagination and query cost, and retain reproducible CI evidence. Use BetaRoast for human feedback on the resulting product experience.

