GraphQL Testing for QA Engineers: Queries, Mutations, Schema Validation, and Security
GraphQL APIs are different from REST. Single endpoint, dynamic queries, nested resolvers, N+1 problems. Traditional API testing tools struggle. Here is how to test GraphQL properly.
🎠Want to master this with real projects? Join the Playwright Automation Mastery course at The Testing Academy.
Contents
GraphQL vs REST Testing Differences
| Aspect | REST | GraphQL |
|---|---|---|
| Endpoints | Many (/users, /orders) | One (/graphql) |
| Response shape | Fixed by server | Defined by client query |
| Status codes | 404, 401, 500 etc. | Usually 200 (errors in body) |
| Over-fetching | Common | Client controls fields |
| Versioning | URL-based (/v2/) | Schema evolution |
Testing GraphQL Queries
test('fetch user with specific fields', async ({ request }) => {
const response = await request.post('/graphql', {
data: {
query: `
query GetUser($id: ID!) {
user(id: $id) {
name
email
orders { id total }
}
}
`,
variables: { id: "1" }
}
});
const { data, errors } = await response.json();
expect(errors).toBeUndefined();
expect(data.user.name).toBeDefined();
expect(data.user.email).toContain('@');
expect(data.user.orders).toBeInstanceOf(Array);
});
Testing Mutations
test('create user mutation', async ({ request }) => {
const response = await request.post('/graphql', {
data: {
query: `
mutation CreateUser($input: CreateUserInput!) {
createUser(input: $input) {
id
name
email
}
}
`,
variables: {
input: { name: "Test User", email: "test@example.com", password: "Pass123!" }
}
}
});
const { data, errors } = await response.json();
expect(errors).toBeUndefined();
expect(data.createUser.id).toBeDefined();
expect(data.createUser.name).toBe("Test User");
});
🚀 Level Up Your Playwright
From locators to CI pipelines — build a production-grade Playwright + TypeScript framework step by step.
GraphQL-Specific Test Cases
- Query depth limiting: Deeply nested queries should be rejected (prevents DoS)
- Field authorization: User role should not see admin-only fields
- N+1 detection: Monitor resolver call count per query
- Introspection control: Introspection should be disabled in production
- Error masking: Internal errors should not leak stack traces
- Batch query limits: Prevent query batching abuse
Schema Validation
test('schema has not changed unexpectedly', async ({ request }) => {
const response = await request.post('/graphql', {
data: { query: '{ __schema { types { name fields { name type { name } } } } }' }
});
const schema = await response.json();
// Compare against saved schema snapshot
expect(schema).toMatchSnapshot('graphql-schema');
});
Security Testing
test('reject deeply nested query (DoS prevention)', async ({ request }) => {
const deepQuery = '{ user(id:"1") { orders { items { product { reviews { author { orders { items { product { name } } } } } } } } } }';
const response = await request.post('/graphql', {
data: { query: deepQuery }
});
const { errors } = await response.json();
expect(errors).toBeDefined();
expect(errors[0].message).toContain('depth');
});
test('introspection disabled in production', async ({ request }) => {
const response = await request.post('/graphql', {
data: { query: '{ __schema { types { name } } }' }
});
const { errors } = await response.json();
expect(errors).toBeDefined();
});
🎓 Master Playwright End to End
Join hundreds of SDETs building real automation frameworks. Lifetime access, hands-on projects, and a job-ready portfolio.
