On this page10 sections
GraphQL — Test a Query and Mutation API
Build a focused test suite for a GraphQL API covering queries, mutations, the errors array, nested authorisation, and the N+1 trap — the concerns that make GraphQL testing different from REST.
Role
API tester
Difficulty
IntermediateTime limit
~120 min
Category
api testing
Scenario
You've joined a team shipping a GraphQL API for an order-management system called GraphCart. The previous tester wrote a handful of happy-path queries and called it covered — but two production incidents have already slipped through: a query that returned `200 OK` with an `errors` array nobody was checking, and an authorisation gap where a logged-in customer could read another customer's payment details through a nested field. Your task is to build a test suite that treats GraphQL on its own terms — asserting on the response body rather than the status code, exercising the schema's type rules, and probing the nested-authorisation and performance traps that REST habits miss. You'll test against a real public GraphQL endpoint so the work is concrete.
Requirements
- 1.Pick a public GraphQL API (or run a local one) and document its endpoint, a representative query operation, and a mutation operation if available; if your chosen API is read-only, simulate the mutation reasoning in writing.
- 2.Write at least six query tests that assert on the response BODY, not the status code: verify `data` is shaped exactly like the requested fields, and that requesting fewer fields returns fewer fields (over-fetching is honoured).
- 3.Write at least four negative tests that trigger the `errors` array — malformed queries, unknown fields, omitted non-null variables — and assert on a stable identifier (`extensions.code` where available) rather than the human-readable `message`.
- 4.Demonstrate the `200-on-error` trap explicitly: show one test where the HTTP status is `200` but the operation logically failed, and explain why a status-only assertion would have passed a broken response.
- 5.Write at least two variable-driven tests: one proving a non-null variable is rejected when omitted, and one proving a default value applies when the variable is omitted.
- 6.Design a nested-authorisation test: given two users, show how you would assert that user A cannot read a protected nested field (e.g. `order.customer.paymentMethod`) belonging to user B, even within an otherwise-permitted query. Describe the setup even if the public API can't fully reproduce it.
- 7.Produce a short written analysis (at least five sentences) of how you would detect the N+1 problem for a list query with a nested resolver — what you'd measure, and why it usually won't surface in a functional assertion.
- 8.Summarise your suite in a coverage table mapping each GraphQL-specific risk (error array, partial success, nullability, nested authz, N+1, introspection exposure) to the test(s) that cover it, and flag any risk you could not cover and why.
Starter data
- ›Public GraphQL endpoints: countries.trevorblades.com (countries/continents, read-only), swapi-graphql (Star Wars), or spin up a local Apollo Server sandbox.
- ›A query has the shape: POST /graphql with body { query, variables, operationName }.
- ›A GraphQL error response carries { data, errors: [{ message, path, extensions: { code } }] } — and data and errors can both be present (partial success).
Expected deliverables
- ✓A runnable or fully-specified test suite (collection export, code repo, or documented request/assertion set) covering the required query, negative, variable, and authorisation cases.
- ✓The explicit `200-on-error` demonstration with explanation.
- ✓The written N+1 detection analysis.
- ✓The risk-to-test coverage table, including honestly-flagged gaps.
Evaluation rubric
| Dimension | What reviewers look for |
|---|---|
| Body-over-status assertions | Does the suite assert on `data` and `errors` rather than HTTP status? A weak submission checks `status === 200` and stops — which passes broken GraphQL responses. A strong one treats the body as the source of truth and only uses status as a transport-level check. |
| Negative and error coverage | Do the negative tests target the `errors` array with stable identifiers? A weak answer asserts on `message` strings that break on copy edits. A strong one keys on `extensions.code` and covers validation errors (unknown field, missing non-null variable) distinctly from resolver errors. |
| Schema-type awareness | Does the candidate exercise nullability and variables as schema concepts? A weak submission treats GraphQL like a JSON-over-HTTP black box. A strong one shows non-null rejection, default-value application, and field-selection honouring as deliberate tests. |
| Nested authorisation reasoning | Does the authz test reason about depth? A weak answer tests only top-level access. A strong one recognises that a single query traverses the graph and that authorisation must hold on nested fields, designing the A-cannot-read-B's-nested-field case explicitly. |
| Performance (N+1) insight | Does the N+1 analysis identify that the bug is invisible to functional assertions and surfaces under load? A weak answer describes N+1 generically. A strong one names what to measure (resolver/DB call count vs list size) and why a passing functional test gives false confidence. |
| Coverage honesty | Does the coverage table map risks to tests and flag genuine gaps? A weak submission claims full coverage. A strong one admits what the chosen public API couldn't reproduce (e.g. real nested authz) and says how it would be covered against a controllable target. |
Sample solution outline
- ›Endpoint chosen: countries.trevorblades.com. Query under test: country(code: "BR") { name capital currency continent { name } }.
- ›Query tests: (1) full selection returns name+capital+currency+continent.name; (2) reduced selection { name } returns ONLY name; (3) nested continent resolves; (4) list query continents { name } returns array; (5) unknown field { country(code:"BR"){ population } } -> errors; (6) nesting depth resolves correctly.
- ›Negative tests: malformed query (syntax) -> errors with GRAPHQL_PARSE_FAILED; unknown field -> GRAPHQL_VALIDATION_FAILED; omitted non-null $code -> validation error; assert on extensions.code not message.
- ›200-on-error demo: unknown-field query returns HTTP 200 + errors[] with data:null — show that status-only assertion passes it.
- ›Variable tests: query($code: ID!){...} omitting code -> rejected; query with default $first: Int = 5 omitted -> 5 results.
- ›Nested authz (described): two tokens; query me { orders { customer { paymentMethod } } }; assert user A's token cannot resolve user B's paymentMethod; note public API can't reproduce, would use a local Apollo sandbox with auth directives.
- ›N+1 analysis: continents { countries { name } } — measure resolver calls; in a real server watch DB hits scale with country count; fix is DataLoader batching; functional test stays green throughout.
- ›Coverage table: error-array ✓, partial-success ✓ (note if reproducible), nullability ✓, nested-authz (described, not run), N+1 (analysed, not measured on public API), introspection (test __schema availability).
Common mistakes
- Asserting `status === 200` and treating the request as passed — the single most common GraphQL testing error, and exactly the incident in the scenario.
- Asserting on the `message` string instead of `extensions.code`, producing tests that break every time someone edits an error message.
- Assuming `data` and `errors` are mutually exclusive — partial success means a response can carry both, and ignoring that hides half-failed queries.
- Testing authorisation only at the top level and missing that a nested field can leak data the user shouldn't see.
- Declaring the N+1 risk 'covered' by a functional test — the functional response is correct; the bug is in call count under load.
- Treating GraphQL as REST-with-one-URL and never exercising the schema's nullability or variable rules.
Submission checklist
- At least six query tests asserting on the response body, including a field-selection test
- At least four negative tests targeting the errors array via extensions.code
- The explicit 200-on-error demonstration with written explanation
- At least two variable tests (non-null rejection + default application)
- A documented nested-authorisation test design
- A written N+1 detection analysis of at least five sentences
- A risk-to-test coverage table with honestly-flagged gaps
Extension ideas
- +Add a query-depth or complexity-limit test: send a deeply nested query and assert the server rejects or limits it.
- +Test introspection per environment: assert __schema is queryable in a dev sandbox but blocked in a hardened config.
- +Wire the suite into CI and gate on the negative tests, proving the 200-on-error case can't regress silently.