What does executing a test document against the real GraphQL schema catch that calling a field resolver function cannot?
answer
- The function is not the endpoint
- Stages run before and after a resolver
- Aliases, fragments, variable coercion
- The schema enforces the type, not the function
- An empty errors list is an assertion
basics
~20 sEverything the executor does around a resolver: coercing variables, applying aliases and fragments, checking each returned value against the field's declared type, propagating non-null failures, and recording every field error with its path in the errors list.
solid answer
~50 sA field resolver is a plain function; calling it proves the function works. GraphQL's value is in the machinery wrapped around it. Executing a document in-process against the built schema runs the whole pipeline: parse, document validation, variable coercion against the declared argument types, field collection (aliases, named and inline fragments, `@skip`/`@include`), the resolvers themselves, then value completion — checking what each resolver returned against the field's type, resolving abstract types, and propagating a null out of a non-null field to the nearest nullable ancestor. That is where the interesting bugs live: a resolver returning a string where the schema says `Int!`, a nullable database column behind a non-null field. Assert on the whole response, not just `data`. A field error leaves the response otherwise healthy, so a test that reads only the keys it cares about will happily pass while the `errors` list is non-empty.
code
graphql · 9 linesquery BinContents($code: ID!) {
location: bin(code: $code) {
aisle
pallets {
sku
quantityOnHand
}
}
}go deeper
Be ready to say that a GraphQL response can carry data and errors at once, and that a passing assertion on data proves nothing until you have also checked that the errors list is empty.
Explain the pipeline in order — parse, validate, coerce variables, collect fields, resolve, complete values — and name a concrete bug each stage catches that a direct call to the resolver function cannot.
Show where you draw the in-process boundary: which behaviours you cover by executing documents against the built schema, and which small set you deliberately cover over real HTTP because the transport owns them.
Own the argument that GraphQL shifts test weight off individual functions and onto documents executed against the schema, and be able to say what that costs in build time and what it removes from other test layers.
## The resolver is the small part A field resolver is an ordinary function. It takes a parent value, coerced arguments and some context, and returns a value or a promise of one. Nothing about it is GraphQL-specific, and a unit test that calls it directly tests exactly that function — which is worth doing, and is not what an interviewer means by "testing a GraphQL API". What makes the endpoint a GraphQL endpoint is the executor sitting around the resolver, and the executor is where most GraphQL-shaped defects are. Executing a document **in-process** means calling the server's execute entry point directly with the built schema, the document text and a variables map — no HTTP server, no port, no network, no client library. It returns the response object, and it is fast enough to run per test. ## The stages a direct resolver call skips Between the document text and the resolver, the executor does the following, in order. **Parse.** A syntax error here is a request error: execution never starts, and the response carries `errors` with no `data` key at all. **Document validation.** The document is checked against the schema — the field exists on that type, the fragment's type condition can apply, the variable's declared type is usable where it is used. The rules themselves and the request-time phase that runs them are a separate subject; what matters here is that an in-process test goes through them and a resolver call does not, so a test document that no longer matches the schema fails your test rather than shipping. **Variable coercion.** Values from the variables map are coerced against the argument types the schema declares, with defaults applied. A failure is a request error, not a field error. **Field collection.** Aliases are applied, named and inline fragments are flattened, `@skip` and `@include` are evaluated, and fields selected twice are merged. A resolver call never sees an alias, so a test that only calls resolvers can never catch a response key being wrong. **Execution and value completion.** After a resolver returns, the executor completes the value against the field's type: coercing scalars, walking lists, resolving which object type an interface or union field actually produced, and enforcing nullability. A resolver returning `null` for a field declared non-null does not produce a null in the response — it raises a field error and nulls the nearest nullable ancestor field, which can propagate all the way to `data: null`. ## The warehouse case ```graphql type Query { bin(code: ID!): Bin } type Bin { code: ID! aisle: String! pallets: [Pallet!]! } type Pallet { id: ID! sku: String! quantityOnHand: Int! } ``` The `Bin.aisle` resolver reads a column that is nullable in the warehouse database, and for 14 legacy bins it is null. The resolver's own unit test passes — the function faithfully returns what the row holds. Executed against the schema, `aisle` is `String!`, so the null is a field error, `Bin` itself is nulled, and because `Query.bin` is nullable the hole stops there: `data.bin` is `null` with one entry in `errors` whose `path` is `["bin", "aisle"]`. No unit test on that function can produce that outcome, because the rule that produced it lives in the schema, not in the function. ## Assert on the envelope, not just on data A GraphQL response body has up to three top-level entries: `data`, `errors` and `extensions`. The specification says `errors` must not be present when nothing went wrong, and `data` must not be present when the request failed before execution. Crucially, a field error does **not** empty the response — you get partial data plus an error, and over HTTP the request itself was well-formed, so the status code alone tells you nothing about whether the fields resolved. That produces the single most common false green in GraphQL testing: the test reads `response.data.bin.pallets` and asserts on its length, the assertion passes, and nobody ever looks at `errors`. Make the absence of `errors` an explicit assertion in every happy-path test. In a test that expects a failure, assert on the error's `path` rather than on its message — the path is structural and stable, the message is prose someone will reword. ## Where the in-process boundary stops In-process execution deliberately excludes the transport. Status codes and media types from the GraphQL over HTTP specification, cookies and auth headers, CORS, request-size limits, batching of several operations in one HTTP request and the WebSocket subprotocol for subscriptions are all outside it, because you never made an HTTP request. It also runs whatever context you hand it, so field authorization is only exercised if you construct a realistic context. Knowing what the in-process test does not cover is as much of the answer as knowing what it does.
- A test asserts on data and passes, yet the field is broken in production. How does that happen?The field errored. A field error nulls that field, appends an entry to `errors` with a `path`, and leaves the rest of `data` intact, so an assertion that reads only the keys it cares about never notices. Over HTTP the request was well-formed, so the status code does not reveal it either. Assert that `errors` is absent on every happy path, and assert on the error `path` when a failure is expected.
- You want a test to prove a document that selects a removed field fails. Where in execution does it fail?Before execution starts. The document is validated against the schema after parsing, and an unknown field is a request error: no resolver runs, and the response carries `errors` with no `data` key at all. That is why the failing test looks nothing like a field error — there is no partial data to inspect, so assert on the shape of the response, not just on its contents.
- What can an in-process execution test never tell you about the deployed endpoint?Anything the transport owns: HTTP status codes and media types, cookies and authentication headers, CORS, request-size limits, several operations batched into one HTTP request, and the WebSocket subprotocol carrying subscriptions. It also only exercises the context object you construct, so field authorization passes trivially unless you build a realistic one. Cover those with a smaller number of tests that actually go over the wire.
Unit-testing a resolver is like testing a warehouse picker in isolation: they fetch the right box. Executing the document is running the whole order — checking the manifest is valid, the labels match, and that one missing item does not silently ship as a complete pallet.
saying these in an interview costs you the question
- Assumes a non-2xx status means the operation failed
- Asserts only on data and never inspects errors
- Thinks a resolver returning null just yields null
- Calls resolvers directly and calls that testing the schema
- Asserts on error message text instead of the error path
- Believes in-process execution covers headers and status codes