API Testing
GraphQL API Testing: Four Gaps That Let Broken Mutations Pass
Good GraphQL API testing proves that a mutation changed the system, not just that it returned a success response: read the effect back from the graph, make setup failures fail loudly, assert on typed errors, and make every test send what its title claims. Each of those is a gap that lets a broken mutation pass.
This post lays out four such gaps, the GraphQL testing best practices that close them, and what it looked like to apply them on a real engagement involving a federated GraphQL graph.
Why GraphQL API tests pass while the system is broken
A passing test only means something if it could have failed. On a typical GraphQL suite, four habits quietly remove that possibility.
- Mutation tests assert only on the mutation's own response payload.
- When test setup fails, the test calls skip, so broken setup shows as skipped and never as failed.
- Typed error members are never selected in the test documents, so they cannot be asserted.
- Some pagination tests do not send what their titles claim.
None of these produce a red build. That is exactly why they survive for so long.
Testing GraphQL mutations: read the effect back from the graph
If a test asserts only on what the mutation returned, a service that sends back a well-formed success without persisting anything still passes. The response and the behavior come from the same place, so the test cannot tell them apart.
The fix is a persistence read-back. After every mutation, query the graph again and assert on the stored state. Centralize this in one module of helpers that every spec calls, so that checking persistence is the default and not something each author remembers to do. When the graph is federated, the read-back also exercises the path through the router and across subgraphs, which is the path your users actually take.
A good read-back helper is deliberately plain. It takes the identifier the mutation returned, queries the entity through the same public graph, and hands the stored fields back for the test to assert on. Name helpers after the entity they read, so a failure message says exactly what was missing and where.
GraphQL error handling: assert the type and the payload
Many schemas model failures as typed error unions, where a mutation can return either its success shape or one of several named error shapes. If the test document never selects those error members, the test cannot see them at all.
The decision that matters most here is to assert the error's type and its payload, not merely that an error occurred. Every negative test should check the returned type name plus the error's contents, such as which identifiers it lists. A test that only asserts that something failed passes when the backend returns the wrong failure, which is exactly the bug you most want to catch.
The same discipline applies to authorization testing. A request from a caller who should be refused needs to come back with the right typed refusal, and the state behind it needs to be unchanged. Pin the precedence rules too. When one request triggers more than one kind of error, decide which wins, write it down, and assert it.
Setup failures and pagination: quiet ways to pass
Skipping a test when setup fails feels polite. In practice it hides defects, because a skipped test is neutral and a red one is not. Turn setup failures into hard failures. If a test truly has to be parked, quarantine it with a linked ticket instead of deleting it, so the cost stays visible and the test comes back when the fix ships. Make the failure message name the setup step that broke, so the person reading a red result knows where to look first.
Pagination deserves a line of its own. If a test claims to check a later page but never sends a cursor or an offset, it is really checking the default page. Read each title, then read the request, and confirm they match.
What this looked like on a real engagement
On a multi-tenant commerce operations platform, an identity and membership domain moved off a REST backend-for-frontend onto GraphQL subgraphs behind a federated router. The domain covers users, user groups, organizations, role grants, store access and third-party application installs. The engagement was API contract and authorization testing on that federated graph, and its existing coverage had all four gaps above.
We built sixty-two API test cases: seventeen for application installs, sixteen for organizations, fifteen for users and fourteen for user groups. A persistence read-back module of sixteen helpers in a single file is called from forty-four sites across twenty-two spec files, so every mutation's effect is re-queried from the graph rather than trusted from its own response. Typed error-union coverage includes an archived-users error arm added to three membership mutations, with the precedence rule pinned: when a request mixes missing and archived users, "not found" wins and lists only the missing identifier. And twenty-two setup-failure skips were converted into hard failures.
The result is sixty-two cases on the integration branch in a fifty-two file suite. Twenty-two tests that previously could not fail now can. Seven tests are quarantined with linked tickets rather than deleted, and one of those quarantines records a real backend defect: a group-hierarchy mutation accepted a cycle, after which the tree query returned a self-repeating path. A test that only checked the mutation's response would never have noticed. You can read the full case study or see all of our case studies.
A GraphQL API testing checklist
- Every mutation test re-queries the graph and asserts on the stored state.
- Persistence checks live in one shared helper module, not in each spec.
- Setup failures fail the test. Nothing skips silently.
- Test documents select the typed error members, and negative tests assert the type name and the contents.
- Precedence rules for mixed errors are written down and pinned by a test.
- Authorization tests check both the refusal and that nothing changed.
- Every pagination test sends the cursor or offset its title describes.
- Quarantined tests carry a linked ticket and are never deleted.
The user interface has its own version of this discipline. If your end-to-end suite is the noisy one, read how to fix flaky Playwright tests. To see how we present findings from an assessment, browse a sample quality audit, or read about our services.
