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

Intermediate

Time limit

~120 min

Category

api testing

Any HTTP client or test library that can POST JSON (Postman, Insomnia, Hoppscotch, or code-based — supertest, REST Assured, etc.)A public GraphQL API to test against (e.g. the Star Wars API at swapi-graphql, the Countries API at countries.trevorblades.com, or a local sandbox)

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

DimensionWhat reviewers look for
Body-over-status assertionsDoes 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 coverageDo 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 awarenessDoes 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 reasoningDoes 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) insightDoes 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 honestyDoes 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.