GraphQL
Testing GraphQL APIs
Why Test GraphQL APIs?
Testing GraphQL APIs ensures your resolvers return correct data, handle errors gracefully, and maintain performance under load. Unlike REST APIs with fixed endpoints, GraphQL's flexible query structure requires comprehensive testing strategies covering schema validation, resolver logic, and integration scenarios.
Testing Setup
Install necessary testing libraries:
npm install --save-dev jest @types/jest
npm install --save-dev @apollo/server
npm install --save-dev graphql-tag
npm install --save-dev supertest
// package.json
{
"scripts": {
"test": "jest",
"test:watch": "jest --watch",
"test:coverage": "jest --coverage"
},
"jest": {
"testEnvironment": "node",
"coveragePathIgnorePatterns": ["/node_modules/"]
}
}
Unit Testing Resolvers
Test individual resolvers in isolation:
// resolvers/userResolvers.js
const resolvers = {
Query: {
user: async (parent, { id }, { dataSources }) => {
return dataSources.userAPI.getUser(id);
},
users: async (parent, args, { dataSources }) => {
return dataSources.userAPI.getAllUsers();
}
},
User: {
posts: async (user, args, { dataSources }) => {
return dataSources.postAPI.getPostsByUserId(user.id);
},
fullName: (user) => {
return `${user.firstName} ${user.lastName}`;
}
},
Mutation: {
createUser: async (parent, { input }, { dataSources }) => {
const newUser = await dataSources.userAPI.createUser(input);
return {
success: true,
message: 'User created successfully',
user: newUser
};
}
}
};
module.exports = resolvers;
// __tests__/userResolvers.test.js
const resolvers = require('../resolvers/userResolvers');
describe('User Resolvers', () => {
describe('Query.user', () => {
it('should return a user by ID', async () => {
// Mock data source
const mockUserAPI = {
getUser: jest.fn().mockResolvedValue({
id: '1',
firstName: 'John',
lastName: 'Doe',
email: 'john@example.com'
})
};
const context = {
dataSources: { userAPI: mockUserAPI }
};
const result = await resolvers.Query.user(
null,
{ id: '1' },
context
);
expect(mockUserAPI.getUser).toHaveBeenCalledWith('1');
expect(result).toEqual({
id: '1',
firstName: 'John',
lastName: 'Doe',
email: 'john@example.com'
});
});
it('should handle errors when user not found', async () => {
const mockUserAPI = {
getUser: jest.fn().mockRejectedValue(new Error('User not found'))
};
const context = {
dataSources: { userAPI: mockUserAPI }
};
await expect(
resolvers.Query.user(null, { id: '999' }, context)
).rejects.toThrow('User not found');
});
});
describe('User.fullName', () => {
it('should concatenate firstName and lastName', () => {
const user = {
firstName: 'Jane',
lastName: 'Smith'
};
const result = resolvers.User.fullName(user);
expect(result).toBe('Jane Smith');
});
});
describe('Mutation.createUser', () => {
it('should create a new user', async () => {
const mockUserAPI = {
createUser: jest.fn().mockResolvedValue({
id: '2',
firstName: 'Alice',
lastName: 'Johnson',
email: 'alice@example.com'
})
};
const context = {
dataSources: { userAPI: mockUserAPI }
};
const input = {
firstName: 'Alice',
lastName: 'Johnson',
email: 'alice@example.com'
};
const result = await resolvers.Mutation.createUser(
null,
{ input },
context
);
expect(mockUserAPI.createUser).toHaveBeenCalledWith(input);
expect(result.success).toBe(true);
expect(result.user.id).toBe('2');
});
});
});
Note: Unit tests should focus on resolver logic only. Mock all external dependencies like databases, APIs, and data sources.
Integration Testing with Apollo Server
Test complete queries and mutations against a real server instance:
// __tests__/integration.test.js
const { ApolloServer } = require('@apollo/server');
const { startStandaloneServer } = require('@apollo/server/standalone');
const gql = require('graphql-tag');
const typeDefs = require('../schema');
const resolvers = require('../resolvers');
describe('GraphQL Integration Tests', () => {
let server;
let url;
beforeAll(async () => {
// Create test server
server = new ApolloServer({
typeDefs,
resolvers,
context: async () => ({
dataSources: {
userAPI: new MockUserAPI(),
postAPI: new MockPostAPI()
}
})
});
// Start server
const { url: serverUrl } = await startStandaloneServer(server, {
listen: { port: 0 } // Random available port
});
url = serverUrl;
});
afterAll(async () => {
await server?.stop();
});
it('should fetch a user with nested posts', async () => {
const query = gql`
query GetUser($id: ID!) {
user(id: $id) {
id
firstName
lastName
fullName
posts {
id
title
}
}
}
`;
const response = await server.executeOperation({
query,
variables: { id: '1' }
});
expect(response.body.singleResult.errors).toBeUndefined();
expect(response.body.singleResult.data.user).toMatchObject({
id: '1',
firstName: 'John',
fullName: 'John Doe'
});
expect(response.body.singleResult.data.user.posts).toBeInstanceOf(Array);
});
it('should handle invalid queries', async () => {
const query = gql`
query {
user(id: "999") {
id
firstName
nonExistentField
}
}
`;
const response = await server.executeOperation({ query });
expect(response.body.singleResult.errors).toBeDefined();
expect(response.body.singleResult.errors[0].message).toContain(
'Cannot query field "nonExistentField"'
);
});
it('should create a user via mutation', async () => {
const mutation = gql`
mutation CreateUser($input: CreateUserInput!) {
createUser(input: $input) {
success
message
user {
id
firstName
email
}
}
}
`;
const response = await server.executeOperation({
query: mutation,
variables: {
input: {
firstName: 'Bob',
lastName: 'Wilson',
email: 'bob@example.com'
}
}
});
expect(response.body.singleResult.errors).toBeUndefined();
expect(response.body.singleResult.data.createUser.success).toBe(true);
expect(response.body.singleResult.data.createUser.user.email).toBe(
'bob@example.com'
);
});
});
Mocking the GraphQL Schema
Use mock resolvers for testing client-side code without a real server:
const { ApolloServer } = require('@apollo/server');
const { addMocksToSchema } = require('@graphql-tools/mock');
const { makeExecutableSchema } = require('@graphql-tools/schema');
// Create schema with mocks
const schema = makeExecutableSchema({ typeDefs });
const mocks = {
Int: () => 42,
Float: () => 3.14,
String: () => 'Mock string',
User: () => ({
id: 'mock-user-id',
firstName: 'Mock',
lastName: 'User',
email: 'mock@example.com'
}),
Post: () => ({
id: 'mock-post-id',
title: 'Mock Post Title',
content: 'Mock post content'
})
};
const schemaWithMocks = addMocksToSchema({
schema,
mocks,
preserveResolvers: false // Replace all resolvers with mocks
});
const server = new ApolloServer({
schema: schemaWithMocks
});
// All queries return mock data
// query { user(id: "1") { firstName } }
// Returns: { "data": { "user": { "firstName": "Mock" } } }
Tip: Use
preserveResolvers: true to keep some real resolvers while mocking others. This is useful for testing specific parts of your schema.
Testing Queries and Mutations
Write comprehensive tests for different query scenarios:
describe('Query Tests', () => {
it('should handle pagination correctly', async () => {
const query = gql`
query GetPosts($limit: Int!, $offset: Int!) {
posts(limit: $limit, offset: $offset) {
id
title
}
}
`;
const response = await server.executeOperation({
query,
variables: { limit: 5, offset: 0 }
});
expect(response.body.singleResult.data.posts).toHaveLength(5);
});
it('should filter results based on arguments', async () => {
const query = gql`
query SearchPosts($searchTerm: String!) {
posts(search: $searchTerm) {
id
title
}
}
`;
const response = await server.executeOperation({
query,
variables: { searchTerm: 'GraphQL' }
});
const posts = response.body.singleResult.data.posts;
posts.forEach(post => {
expect(post.title.toLowerCase()).toContain('graphql');
});
});
it('should handle authentication errors', async () => {
const query = gql`
query GetMe {
me {
id
email
}
}
`;
// Execute without authentication
const response = await server.executeOperation({ query });
expect(response.body.singleResult.errors).toBeDefined();
expect(response.body.singleResult.errors[0].message).toContain(
'Not authenticated'
);
});
});
describe('Mutation Tests', () => {
it('should validate input data', async () => {
const mutation = gql`
mutation CreateUser($input: CreateUserInput!) {
createUser(input: $input) {
success
message
}
}
`;
// Invalid email
const response = await server.executeOperation({
query: mutation,
variables: {
input: {
firstName: 'Test',
lastName: 'User',
email: 'invalid-email'
}
}
});
expect(response.body.singleResult.data.createUser.success).toBe(false);
expect(response.body.singleResult.data.createUser.message).toContain(
'Invalid email'
);
});
it('should handle concurrent mutations', async () => {
const mutation = gql`
mutation CreatePost($input: CreatePostInput!) {
createPost(input: $input) {
success
post { id }
}
}
`;
// Execute multiple mutations concurrently
const promises = Array.from({ length: 10 }, (_, i) =>
server.executeOperation({
query: mutation,
variables: {
input: {
title: `Post ${i}`,
content: `Content ${i}`
}
}
})
);
const responses = await Promise.all(promises);
responses.forEach(response => {
expect(response.body.singleResult.errors).toBeUndefined();
expect(response.body.singleResult.data.createPost.success).toBe(true);
});
});
});
Snapshot Testing
Use snapshot testing to catch unexpected schema or response changes:
const { printSchema } = require('graphql');
const { makeExecutableSchema } = require('@graphql-tools/schema');
describe('Schema Snapshot Tests', () => {
it('should match schema snapshot', () => {
const schema = makeExecutableSchema({ typeDefs, resolvers });
const schemaString = printSchema(schema);
expect(schemaString).toMatchSnapshot();
});
it('should match query response snapshot', async () => {
const query = gql`
query GetUser {
user(id: "1") {
id
firstName
lastName
email
posts {
id
title
}
}
}
`;
const response = await server.executeOperation({ query });
expect(response.body.singleResult.data).toMatchSnapshot();
});
});
// First run creates snapshot file
// Subsequent runs compare against snapshot
// If schema/response changes, test fails
// Run `jest -u` to update snapshots
Warning: Snapshot tests are useful for detecting unintended changes, but they can become brittle. Use them selectively for critical schema structures and always review snapshot changes carefully.
Testing Utilities
Create reusable test utilities and helpers:
// testUtils.js
const { ApolloServer } = require('@apollo/server');
// Create test server with mocked context
function createTestServer(options = {}) {
const {
typeDefs,
resolvers,
context = {}
} = options;
return new ApolloServer({
typeDefs,
resolvers,
context: async () => ({
user: null,
dataSources: {},
...context
})
});
}
// Execute query helper
async function executeQuery(server, query, variables = {}) {
const response = await server.executeOperation({
query,
variables
});
if (response.body.singleResult.errors) {
throw new Error(response.body.singleResult.errors[0].message);
}
return response.body.singleResult.data;
}
// Authenticated context helper
function authenticatedContext(userId) {
return {
user: {
id: userId,
role: 'USER'
}
};
}
// Admin context helper
function adminContext(userId) {
return {
user: {
id: userId,
role: 'ADMIN'
}
};
}
module.exports = {
createTestServer,
executeQuery,
authenticatedContext,
adminContext
};
// Usage in tests
const { createTestServer, executeQuery, authenticatedContext } = require('./testUtils');
describe('Using Test Utilities', () => {
it('should fetch user data with authentication', async () => {
const server = createTestServer({
typeDefs,
resolvers,
context: authenticatedContext('user-123')
});
const query = gql`
query GetMe {
me {
id
email
}
}
`;
const data = await executeQuery(server, query);
expect(data.me.id).toBe('user-123');
});
});
Testing Error Handling
Verify that errors are handled and formatted correctly:
describe('Error Handling Tests', () => {
it('should format validation errors', async () => {
const mutation = gql`
mutation CreatePost($input: CreatePostInput!) {
createPost(input: $input) {
success
message
}
}
`;
const response = await server.executeOperation({
query: mutation,
variables: {
input: {
title: '', // Empty title - validation error
content: 'Test content'
}
}
});
expect(response.body.singleResult.data.createPost.success).toBe(false);
expect(response.body.singleResult.data.createPost.message).toBe(
'Title is required'
);
});
it('should handle authorization errors', async () => {
const mutation = gql`
mutation DeleteUser($id: ID!) {
deleteUser(id: $id) {
success
}
}
`;
// Execute without admin privileges
const response = await server.executeOperation({
query: mutation,
variables: { id: 'user-123' }
});
expect(response.body.singleResult.errors).toBeDefined();
expect(response.body.singleResult.errors[0].extensions.code).toBe(
'FORBIDDEN'
);
});
});
Exercise: Create a comprehensive test suite for a GraphQL blog API with:
- Unit tests for all query and mutation resolvers
- Integration tests for creating, updating, and deleting posts
- Tests for pagination and filtering
- Authentication and authorization tests
- Error handling tests for invalid input
- A snapshot test for the complete schema