Q21 of 37 · API testing
How do you test an API that uses GraphQL?
Short answer
Short answer: Send POST requests to the single endpoint with a query/mutation in the body; assert on `data` and `errors`. Test query shape (only requested fields return), error handling (errors come back in 200s with an `errors` array), and schema introspection. Use GraphQL Codegen for type-safe test queries.
Detail
GraphQL changes the testing model in three ways: one endpoint instead of many, errors travel inside 200 responses, and clients pick fields explicitly.
The basic pattern:
test('user query returns requested fields', async ({ request }) => {
const res = await request.post('/graphql', {
data: {
query: `query GetUser($id: ID!) {
user(id: $id) { id email role }
}`,
variables: { id: '42' },
},
});
expect(res.status()).toBe(200);
const { data, errors } = await res.json();
expect(errors).toBeUndefined();
expect(data.user).toEqual({
id: '42',
email: expect.stringMatching(/@/),
role: expect.any(String),
});
});
Key differences from REST:
1. Single endpoint, multiple operations. Everything goes to POST /graphql. Routing is in the query body, not the URL. Tests don't have endpoint-level coverage to worry about.
2. Status code is almost always 200. Even validation errors and authorisation failures often return 200 with an errors array. Test by checking errors, not status code:
expect(errors[0].extensions.code).toBe('UNAUTHENTICATED');
3. Field-level selection. The client picks fields. Test patterns:
- Query only the fields you need; the response should contain only those.
- Query a non-existent field → 400 / parse error.
- Query a field requiring auth →
errorswith auth code.
4. Mutations. Same shape, different intent:
mutation CreateUser($input: CreateUserInput!) {
createUser(input: $input) {
id email
}
}
5. Subscriptions (websockets). Outside the scope of basic API tests; needs a websocket-aware client.
Test patterns specific to GraphQL:
Schema introspection — { __schema { types { name } } }. Useful in CI to detect schema drift between consumer and provider.
N+1 detection. Some queries trigger N+1 backend queries; tests can assert on response time or via a SQL counter (if your test infra exposes one).
Authorisation per field. Some fields are admin-only. Run the same query with different roles; confirm restricted fields appear or return null.
Persisted queries (for production performance). Many GraphQL APIs only accept pre-registered queries by hash. Tests need to register the query first or use a dev mode that allows ad-hoc.
Tools:
- GraphQL Codegen — generates TS types from your schema, so test queries are type-safe.
- GraphiQL / Apollo Sandbox — explore the schema interactively.
- Pact + GraphQL — contract testing for GraphQL is supported but less mature than REST.
Common pitfalls:
- Asserting on HTTP 200 = success. GraphQL errors are inside 200 responses.
- Querying with
nullvariables and not testing the case where the field is genuinely null vs not provided. - Ignoring the
extensionsfield, which often carries useful error metadata.
// EXAMPLE
user.graphql.test.ts
import { test, expect } from '@playwright/test';
test('createUser mutation', async ({ request }) => {
const res = await request.post('/graphql', {
data: {
query: `
mutation CreateUser($input: CreateUserInput!) {
createUser(input: $input) {
id email role
}
}
`,
variables: {
input: { email: 'alice@example.com', role: 'viewer' },
},
},
});
expect(res.status()).toBe(200);
const { data, errors } = await res.json();
expect(errors).toBeUndefined();
expect(data.createUser.email).toBe('alice@example.com');
});