GraphQL has revolutionized API development by giving clients precise control over the data they receive. But this flexibility introduces unique testing challenges. Unlike REST where endpoints are fixed, GraphQL allows infinite query combinations. How do you test that? Where do you even start?
This guide covers everything you need to test GraphQL APIs effectively: queries, mutations, subscriptions, schema validation, error handling, and performance testing. You'll learn practical strategies to catch bugs early and ensure your GraphQL API works reliably in production.
GraphQL Testing Fundamentals
What Makes GraphQL Different?
GraphQL fundamentally changes how you test APIs compared to REST.
REST API Testing:
- • Fixed endpoints (GET /users, POST /users)
- • Fixed response structure
- • Multiple endpoints to test
- • Over-fetching or under-fetching common
GraphQL API Testing:
- • Single endpoint (POST /graphql)
- • Dynamic response structure
- • Infinite query combinations
- • Client controls data shape
GraphQL Schema - Your Contract
The schema defines what's possible. Test that your API conforms to the schema.
# Example GraphQL Schema
type User {
id: ID!
name: String!
email: String!
posts: [Post!]!
createdAt: DateTime!
}
type Post {
id: ID!
title: String!
content: String!
author: User!
published: Boolean!
comments: [Comment!]!
}
type Query {
user(id: ID!): User
users(limit: Int, offset: Int): [User!]!
post(id: ID!): Post
posts(authorId: ID): [Post!]!
}
type Mutation {
createUser(name: String!, email: String!): User!
updateUser(id: ID!, name: String, email: String): User!
deleteUser(id: ID!): Boolean!
createPost(title: String!, content: String!): Post!
}
type Subscription {
postAdded: Post!
userUpdated(userId: ID!): User!
}Testing GraphQL Queries
✓ Basic Query Testing
Start with simple queries and verify the response structure matches your expectations.
// Test: Fetch single user
POST /graphql
Content-Type: application/json
{
"query": "{ user(id: \"123\") { id name email } }"
}
// Expected Response:
{
"data": {
"user": {
"id": "123",
"name": "John Doe",
"email": "[email protected]"
}
}
}
// Test Cases:
// ✓ Returns correct user data
// ✓ Only returns requested fields (not all fields)
// ✓ Returns null for non-existent user
// ✓ Response matches schema types (id: ID!, name: String!)✓ Testing Nested Queries
GraphQL's power is nested queries. Test that relationships are resolved correctly.
// Query with nested relationships
{
"query": `
{
user(id: "123") {
id
name
posts {
id
title
comments {
id
text
author {
name
}
}
}
}
}
`
}
// Test Cases:
// ✓ User data is correct
// ✓ Posts array contains user's posts
// ✓ Each post has comments
// ✓ Each comment has author with name
// ✓ No N+1 query problem (check query count)
// ✓ Circular references handled (user > posts > author > posts...)✓ Testing Query Arguments
Test that filtering, pagination, and sorting arguments work correctly.
// Pagination test
{
"query": `
{
users(limit: 10, offset: 20) {
id
name
}
}
`
}
// Test Cases:
// ✓ Returns exactly 10 users
// ✓ Skips first 20 users (offset works)
// ✓ limit: 0 returns empty array
// ✓ Negative limit/offset handled gracefully
// ✓ limit > max limit is capped
// Filtering test
{
"query": `
{
posts(authorId: "123", published: true) {
id
title
published
}
}
`
}
// Test Cases:
// ✓ Only returns posts by author 123
// ✓ Only returns published posts
// ✓ Multiple filters work together (AND logic)
// ✓ Invalid authorId returns empty array⚠The N+1 Query Problem
The most common GraphQL performance killer. One query triggers hundreds of database queries.
The Problem:
Query: Get 100 users and their posts
{
users {
name
posts { // Problem here!
title
}
}
}Result: 1 query for users + 100 queries for posts = 101 queries!
The Solution: DataLoader
Use DataLoader to batch and cache database queries:
- ✓ Batches requests within single tick
- ✓ Caches results within single request
- ✓ 101 queries → 2 queries (1 for users, 1 batched for all posts)
// Test for N+1 problem
test('Should not cause N+1 queries', async () => {
// Enable query logging
const queryCount = 0;
await graphql({
schema,
source: `
{
users {
name
posts { title }
}
}
`
});
// Should be 2 queries max (users + posts batch)
// Not 101 queries!
expect(queryCount).toBeLessThan(5);
});Testing GraphQL Mutations
✓ Testing Create Mutations
Mutations modify data. Test that changes actually happen and response is correct.
// Create user mutation
{
"query": `
mutation {
createUser(name: "Jane Doe", email: "[email protected]") {
id
name
email
createdAt
}
}
`
}
// Expected Response:
{
"data": {
"createUser": {
"id": "456",
"name": "Jane Doe",
"email": "[email protected]",
"createdAt": "2025-01-11T10:30:00Z"
}
}
}
// Test Cases:
// ✓ Returns created user with ID
// ✓ User actually created in database
// ✓ Can query for user after creation
// ✓ Required fields validated (name, email required)
// ✓ Email format validated
// ✓ Duplicate email rejected
// ✓ Returns meaningful error for invalid data✓ Testing Update Mutations
Verify updates work correctly and only change specified fields.
// Update user mutation (partial update)
{
"query": `
mutation {
updateUser(id: "123", name: "John Smith") {
id
name
email
}
}
`
}
// Test Cases:
// ✓ Name updated to "John Smith"
// ✓ Email unchanged (not provided in mutation)
// ✓ Returns 404 error for non-existent user
// ✓ Validates updated fields (email format if provided)
// ✓ Only user owner can update (authorization)
// ✓ Optimistic locking prevents concurrent update conflicts✓ Testing Delete Mutations
Test deletion and verify data is actually removed (or soft-deleted).
// Delete user mutation
{
"query": `
mutation {
deleteUser(id: "123")
}
`
}
// Expected Response:
{
"data": {
"deleteUser": true
}
}
// Test Cases:
// ✓ Returns true on successful delete
// ✓ User actually deleted from database
// ✓ Querying for deleted user returns null
// ✓ Related data handled (cascade delete or set null)
// ✓ Returns false/error for non-existent user
// ✓ Cannot delete if user has dependencies
// ✓ Only authorized users can delete
// ✓ Soft delete works (if implemented)✓ Testing Mutation Variables
Use variables instead of inline values. Cleaner and safer.
// Using variables (recommended)
{
"query": `
mutation CreateUser($name: String!, $email: String!) {
createUser(name: $name, email: $email) {
id
name
email
}
}
`,
"variables": {
"name": "Jane Doe",
"email": "[email protected]"
}
}
// Benefits:
// ✓ Type checking on variables
// ✓ Prevents injection attacks
// ✓ Reusable queries
// ✓ Easier to test with different values
// Test with edge cases:
"variables": {
"name": "", // Empty name
"email": "not-an-email" // Invalid email
}
// Should return validation errorsTesting GraphQL Subscriptions
What Are Subscriptions?
Subscriptions enable real-time updates via WebSocket. Client subscribes to events and receives updates when they occur.
// Client subscribes to new posts
{
"query": `
subscription {
postAdded {
id
title
author {
name
}
}
}
`
}
// Server sends update when post is created:
{
"data": {
"postAdded": {
"id": "789",
"title": "New Post",
"author": { "name": "John" }
}
}
}✓ Testing Subscription Events
Test that subscriptions receive updates when relevant events occur.
// Example test with Apollo Client
test('Should receive update when post is created', async () => {
// 1. Subscribe to postAdded
const subscription = client.subscribe({
query: gql`
subscription {
postAdded {
id
title
}
}
`
});
const updates = [];
subscription.subscribe({
next: (data) => updates.push(data)
});
// 2. Create a post (trigger event)
await client.mutate({
mutation: gql`
mutation {
createPost(title: "Test Post", content: "...") {
id
}
}
`
});
// 3. Wait for subscription update
await waitFor(() => updates.length > 0);
// 4. Verify received data
expect(updates[0].data.postAdded.title).toBe("Test Post");
});
// Test Cases:
// ✓ Subscription receives event
// ✓ Data structure matches schema
// ✓ Only subscribed clients receive update
// ✓ Filtered subscriptions work (e.g., userId filter)
// ✓ Connection handles disconnects gracefully
// ✓ Reconnection works after network issue✓ Testing Subscription Filters
Subscriptions often have filters. Test that clients only receive relevant updates.
// Subscribe to specific user's updates only
{
"query": `
subscription {
userUpdated(userId: "123") {
id
name
email
}
}
`
}
// Test Cases:
// ✓ Receives updates for userId "123"
// ✓ Does NOT receive updates for other users
// ✓ Multiple clients can subscribe to same user
// ✓ Invalid userId handled gracefully
// ✓ Authorization checked (can only subscribe to allowed users)Testing Error Handling
GraphQL Error Format
GraphQL has standardized error format. Partial success is possible - some data succeeds, some fails.
// Query that partially fails
{
"query": `
{
user(id: "123") { name }
user(id: "999") { name } // Doesn't exist
}
`
}
// Response with partial success:
{
"data": {
"user": {
"name": "John Doe"
},
"user": null // Failed, see errors
},
"errors": [
{
"message": "User not found",
"locations": [{ "line": 3, "column": 5 }],
"path": ["user"],
"extensions": {
"code": "NOT_FOUND",
"userId": "999"
}
}
]
}
// Test Cases:
// ✓ Successful fields return data
// ✓ Failed fields return null
// ✓ errors array contains error details
// ✓ Error includes path to failed field
// ✓ Error extensions provide contextCommon GraphQL Errors to Test
Validation Errors
Query doesn't match schema (wrong field names, types, arguments)
Field "invalidField" doesn't exist on type "User"Not Found Errors
Resource doesn't exist
User with ID "999" not foundAuthorization Errors
User doesn't have permission
Not authorized to access this userInput Validation Errors
Invalid input data (email format, required fields, etc.)
Invalid email formatServer Errors
Database connection failed, unhandled exception
Internal server errorSchema Validation & Testing
✓ Test Schema Changes Don't Break Clients
Schema changes can break client apps. Use schema diff tools to catch breaking changes.
// Safe changes (backward compatible): ✓ Add new type ✓ Add new field to existing type ✓ Add new query/mutation ✓ Add new optional argument ✓ Make required argument optional // Breaking changes (backward incompatible): ✗ Remove type, field, or query ✗ Rename type or field ✗ Change field type (String → Int) ✗ Add required argument to existing field ✗ Remove argument from field ✗ Make optional argument required // Use tools to detect: # GraphQL Inspector npx graphql-inspector diff old-schema.graphql new-schema.graphql # Apollo Studio Schema Checks apollo service:check --variant=production
✓ Validate Schema Follows Best Practices
Use linting tools to ensure schema quality and consistency.
// GraphQL ESLint rules
{
"rules": {
"naming-convention": ["error", {
"types": "PascalCase",
"fields": "camelCase",
"enums": "UPPER_CASE"
}],
"require-description": "error",
"no-deprecated": "warn",
"require-id-when-available": "error"
}
}
// Check:
// ✓ Consistent naming (PascalCase for types, camelCase for fields)
// ✓ All types have descriptions
// ✓ IDs are properly typed as ID!
// ✓ Deprecated fields have deprecationReason
// ✓ No unused types in schemaGraphQL Testing Tools
Apollo Client Testing Utilities
If you use Apollo Client, use MockedProvider for testing without real server.
import { MockedProvider } from '@apollo/client/testing';
const mocks = [
{
request: {
query: GET_USER,
variables: { id: '123' }
},
result: {
data: {
user: { id: '123', name: 'John Doe' }
}
}
}
];
test('Should render user name', async () => {
render(
<MockedProvider mocks={mocks} addTypename={false}>
<UserComponent userId="123" />
</MockedProvider>
);
await waitFor(() => {
expect(screen.getByText('John Doe')).toBeInTheDocument();
});
});GraphQL Code Generator
Generate TypeScript types from schema for type-safe testing.
# Install
npm install @graphql-codegen/cli
# codegen.yml
schema: http://localhost:4000/graphql
generates:
./types.ts:
plugins:
- typescript
- typescript-operations
# Generate types
npm run codegen
// Now you have type-safe queries:
const query: GetUserQuery = await client.query({
query: GET_USER,
variables: { id: '123' } // TypeScript knows this is ID!
});GraphQL Playground / GraphiQL
Interactive GraphQL IDE for manual testing. Explore schema, run queries, see docs.
Features:
- ✓ Auto-completion based on schema
- ✓ Built-in documentation explorer
- ✓ Query history
- ✓ Variables and headers support
- ✓ Subscription testing
GraphQL Testing Best Practices
✓ DO
- ✓Test queries with different field selections
- ✓Test nested queries and relationships
- ✓Monitor and test for N+1 query problems
- ✓Test error handling and partial failures
- ✓Validate schema changes for breaking changes
- ✓Use variables instead of inline arguments
- ✓Test authorization at field level
✗ DON'T
- ✗Only test one field combination
- ✗Ignore performance testing (N+1 problem)
- ✗Make breaking schema changes without testing
- ✗Return raw database errors to clients
- ✗Skip authorization testing
- ✗Forget to test subscriptions
- ✗Expose internal errors via extensions
Related Tools & Resources
External References
Official Documentation & Resources
- GraphQL Official Documentation - Learn GraphQL fundamentals
- Apollo GraphQL Documentation - Apollo Client and Server guides
- DataLoader - Solve N+1 query problem
- GraphQL Inspector - Schema diff and validation tool
- GraphQL Code Generator - Generate TypeScript types from schema
Conclusion
GraphQL testing requires a different mindset than REST API testing. The flexibility of GraphQL - where clients control data shape - means you need to test more combinations and watch for performance pitfalls like the N+1 query problem. But with the right strategies and tools, you can build confidence that your GraphQL API works correctly.
Start with basic query and mutation tests, then expand to nested queries, subscriptions, and error handling. Use DataLoader to prevent N+1 queries, validate your schema changes for backward compatibility, and leverage tools like Apollo Client testing utilities and GraphQL Code Generator. Remember to test authorization at the field level and monitor performance as your graph grows. A well-tested GraphQL API gives clients the flexibility they want with the reliability they need.