Testing StrategiesIntermediate
GraphQL API Testing
Test GraphQL APIs effectively with queries, mutations, and subscriptions
7 min read
...
graphqlapi-testingtesting
Introduction
GraphQL APIs require different testing approaches compared to REST APIs. This guide covers strategies for testing queries, mutations, subscriptions, and GraphQL-specific challenges.
GraphQL Fundamentals for Testing
Key Differences from REST
REST API:
- Multiple endpoints (one per resource)
- Fixed response structure
- Over-fetching or under-fetching common
GraphQL API:
- Single endpoint (
/graphql) - Flexible queries (request exactly what you need)
- Strong typing with schema
GraphQL Operation Types
- Query - Read data (GET equivalent)
- Mutation - Modify data (POST/PUT/DELETE equivalent)
- Subscription - Real-time updates via WebSocket
Testing GraphQL Queries
Basic Query Testing
// Example GraphQL query
const query = `
query GetUser($id: ID!) {
user(id: $id) {
id
name
email
posts {
title
createdAt
}
}
}
`;
// Test with fetch
test('should fetch user data', async () => {
const response = await fetch('https://api.example.com/graphql', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
query,
variables: { id: '123' }
})
});
const { data, errors } = await response.json();
expect(errors).toBeUndefined();
expect(data.user).toBeDefined();
expect(data.user.id).toBe('123');
expect(data.user.posts).toBeInstanceOf(Array);
});Using GraphQL Client Libraries
// Using Apollo Client in tests
import { ApolloClient, InMemoryCache, gql } from '@apollo/client';
const client = new ApolloClient({
uri: 'https://api.example.com/graphql',
cache: new InMemoryCache()
});
test('query with Apollo Client', async () => {
const GET_USER = gql`
query GetUser($id: ID!) {
user(id: $id) {
id
name
email
}
}
`;
const { data } = await client.query({
query: GET_USER,
variables: { id: '123' }
});
expect(data.user.name).toBe('John Doe');
});Testing Nested Queries
test('should fetch nested data correctly', async () => {
const query = `
query {
organization(id: "org1") {
name
teams {
name
members {
name
role
}
}
}
}
`;
const { data } = await executeQuery(query);
expect(data.organization.teams).toHaveLength(3);
expect(data.organization.teams[0].members).toBeDefined();
});Testing GraphQL Mutations
Basic Mutation Testing
test('should create a new post', async () => {
const mutation = `
mutation CreatePost($input: CreatePostInput!) {
createPost(input: $input) {
id
title
content
author {
name
}
}
}
`;
const variables = {
input: {
title: 'Test Post',
content: 'This is a test',
authorId: '123'
}
};
const { data, errors } = await executeMutation(mutation, variables);
expect(errors).toBeUndefined();
expect(data.createPost.id).toBeDefined();
expect(data.createPost.title).toBe('Test Post');
});Testing Mutation Side Effects
test('mutation should update database', async () => {
// Create a post
const createMutation = `
mutation {
createPost(input: { title: "New Post", content: "Content" }) {
id
}
}
`;
const { data: createData } = await executeMutation(createMutation);
const postId = createData.createPost.id;
// Verify it was created by querying
const query = `
query {
post(id: "${postId}") {
title
content
}
}
`;
const { data: queryData } = await executeQuery(query);
expect(queryData.post.title).toBe('New Post');
});Testing GraphQL Subscriptions
WebSocket Subscription Testing
import { createClient } from 'graphql-ws';
test('should receive real-time updates', async () => {
const client = createClient({
url: 'ws://api.example.com/graphql'
});
const subscription = `
subscription OnPostCreated {
postCreated {
id
title
author {
name
}
}
}
`;
const messages = [];
await new Promise((resolve) => {
client.subscribe(
{ query: subscription },
{
next: (data) => {
messages.push(data);
if (messages.length === 2) resolve();
},
error: (err) => { throw err; }
}
);
// Trigger events that should cause subscriptions
setTimeout(() => createPost({ title: 'Post 1' }), 100);
setTimeout(() => createPost({ title: 'Post 2' }), 200);
});
expect(messages).toHaveLength(2);
expect(messages[0].data.postCreated.title).toBe('Post 1');
});Schema Validation Testing
Introspection Query Testing
test('should validate schema types', async () => {
const introspectionQuery = `
query {
__schema {
types {
name
kind
fields {
name
type {
name
}
}
}
}
}
`;
const { data } = await executeQuery(introspectionQuery);
const userType = data.__schema.types.find(t => t.name === 'User');
expect(userType).toBeDefined();
expect(userType.fields.find(f => f.name === 'email')).toBeDefined();
});Schema Compatibility Testing
// Test backward compatibility when schema changes
test('deprecated fields still work', async () => {
const query = `
query {
user(id: "123") {
oldFieldName @deprecated(reason: "Use newFieldName instead")
newFieldName
}
}
`;
const { data, errors } = await executeQuery(query);
// Both should work during deprecation period
expect(data.user.oldFieldName).toBe(data.user.newFieldName);
});Error Handling Testing
Testing GraphQL Errors
test('should handle validation errors', async () => {
const query = `
query {
user(id: "invalid-id-format") {
name
}
}
`;
const { data, errors } = await executeQuery(query);
expect(errors).toBeDefined();
expect(errors[0].message).toContain('Invalid ID format');
expect(errors[0].extensions.code).toBe('VALIDATION_ERROR');
});
test('should handle not found errors', async () => {
const query = `
query {
user(id: "999999") {
name
}
}
`;
const { data, errors } = await executeQuery(query);
expect(data.user).toBeNull();
expect(errors[0].extensions.code).toBe('NOT_FOUND');
});Testing Partial Errors
test('should handle partial success', async () => {
const query = `
query {
validUser: user(id: "123") { name }
invalidUser: user(id: "999") { name }
}
`;
const { data, errors } = await executeQuery(query);
// One field succeeds, one fails
expect(data.validUser).toBeDefined();
expect(data.invalidUser).toBeNull();
expect(errors).toHaveLength(1);
});Performance Testing
Query Complexity Testing
test('should reject overly complex queries', async () => {
const complexQuery = `
query {
users {
posts {
comments {
author {
posts {
comments {
# Too many nested levels
}
}
}
}
}
}
}
`;
const { errors } = await executeQuery(complexQuery);
expect(errors[0].extensions.code).toBe('QUERY_TOO_COMPLEX');
});N+1 Query Problem Testing
test('should efficiently fetch nested data', async () => {
const startTime = Date.now();
const query = `
query {
posts(limit: 10) {
title
author {
name
}
}
}
`;
await executeQuery(query);
const duration = Date.now() - startTime;
// Should use DataLoader or similar batching
expect(duration).toBeLessThan(1000);
});Authorization Testing
Testing Field-Level Permissions
test('unauthorized user cannot access private data', async () => {
const query = `
query {
user(id: "123") {
name
email # Only owner can see
}
}
`;
const { data, errors } = await executeQuery(query, {
headers: { Authorization: 'Bearer unauthorized-token' }
});
expect(data.user.name).toBeDefined();
expect(data.user.email).toBeNull();
expect(errors[0].message).toContain('Not authorized');
});Best Practices
1. Test Data Management
- Use GraphQL operations to set up test data
- Clean up after tests with delete mutations
- Use GraphQL transactions if available
2. Organize Tests by Features
describe('User Operations', () => {
describe('Queries', () => {
test('getUser', () => {});
test('listUsers', () => {});
});
describe('Mutations', () => {
test('createUser', () => {});
test('updateUser', () => {});
});
});3. Use Fragments for Reusability
const USER_FRAGMENT = `
fragment UserFields on User {
id
name
email
}
`;
const query = `
${USER_FRAGMENT}
query {
user(id: "123") {
...UserFields
}
}
`;4. Mock GraphQL Responses
import { MockedProvider } from '@apollo/client/testing';
const mocks = [{
request: {
query: GET_USER,
variables: { id: '123' }
},
result: {
data: { user: { id: '123', name: 'John' } }
}
}];
// Use in tests
<MockedProvider mocks={mocks}>
<UserComponent />
</MockedProvider>Tools and Libraries
Popular GraphQL Testing Tools:
- Apollo Client - Full-featured GraphQL client
- graphql-request - Minimal GraphQL client
- EasyGraphQL Tester - Schema-based testing
- GraphQL Playground - Interactive testing
- Insomnia/Postman - Manual API testing
Conclusion
GraphQL testing requires understanding of queries, mutations, subscriptions, and schema validation. Use proper error handling, test authorization, and monitor performance to ensure API quality.
Next Steps
- Explore your GraphQL schema with introspection
- Write tests for critical queries and mutations
- Set up subscription testing for real-time features
- Implement schema validation in CI/CD
- Monitor query performance and complexity
Related Articles
Comments (0)
Loading comments...