Testing GraphQL APIs
A well-tested GraphQL API catches bugs before they reach users and gives you confidence to refactor schema and resolvers. GraphQL APIs have three distinct layers to test: individual resolvers (unit tests), the schema and resolver integration (integration tests), and the full HTTP stack end-to-end.
Three Levels of GraphQL Testing
Level What you test Speed ───── ───────────── ───── Unit Individual resolver functions Fast Integration Schema + resolvers together Medium End-to-End Full HTTP server with real queries Slow
Unit Testing a Resolver
A resolver is a plain function. Test it like any other function — call it directly with mock arguments and assert the output. No server or HTTP needed.
// resolver.js
export const userResolver = async (_, args, context) => {
const user = await context.db.users.findById(args.id);
if (!user) throw new Error('User not found');
return user;
};
// resolver.test.js (Jest)
import { userResolver } from './resolver';
describe('userResolver', () => {
test('returns user when found', async () => {
const mockUser = { id: '1', name: 'Anil' };
const context = {
db: { users: { findById: jest.fn().mockResolvedValue(mockUser) } }
};
const result = await userResolver(null, { id: '1' }, context);
expect(result).toEqual(mockUser);
});
test('throws when user not found', async () => {
const context = {
db: { users: { findById: jest.fn().mockResolvedValue(null) } }
};
await expect(userResolver(null, { id: '999' }, context))
.rejects.toThrow('User not found');
});
});
Integration Testing with executeOperation
Apollo Server's executeOperation method runs a full GraphQL operation — parsing, validation, and resolver execution — without starting an HTTP server. This tests the schema and resolvers together in fast, isolated tests.
npm install --save-dev jest @apollo/server
import { ApolloServer } from '@apollo/server';
import { typeDefs, resolvers } from './schema';
const server = new ApolloServer({ typeDefs, resolvers });
test('GetUser query returns user', async () => {
const response = await server.executeOperation({
query: `
query GetUser($id: ID!) {
user(id: $id) { name email }
}
`,
variables: { id: '1' }
});
expect(response.body.kind).toBe('single');
expect(response.body.singleResult.errors).toBeUndefined();
expect(response.body.singleResult.data?.user).toEqual({
name: 'Anil Kumar',
email: 'anil@example.com'
});
});
Testing Mutations
test('CreatePost mutation creates a post', async () => {
const response = await server.executeOperation({
query: `
mutation CreatePost($input: CreatePostInput!) {
createPost(input: $input) {
id
title
}
}
`,
variables: {
input: { title: 'Hello GraphQL', content: 'This is my first post' }
}
});
const data = response.body.singleResult.data?.createPost;
expect(data?.title).toBe('Hello GraphQL');
expect(data?.id).toBeTruthy();
});
Testing Error Cases
test('returns UNAUTHENTICATED error for protected query', async () => {
const response = await server.executeOperation(
{ query: `{ myProfile { name } }` },
{ contextValue: { user: null, db: mockDb } } ← no user
);
const errors = response.body.singleResult.errors;
expect(errors?.[0].extensions?.code).toBe('UNAUTHENTICATED');
});
Mocking the Database in Tests
Pass a mock database through context so tests never touch a real database. Tests run faster, stay isolated, and work in any CI environment.
const mockDb = {
users: {
findById: jest.fn().mockResolvedValue({ id: '1', name: 'Anil' }),
findAll: jest.fn().mockResolvedValue([
{ id: '1', name: 'Anil' },
{ id: '2', name: 'Priya' }
])
}
};
// Each test gets a fresh context:
const contextValue = { db: mockDb, user: { id: '1', role: 'USER' } };
Key Points
- Test resolvers as plain functions with mock context — fast and isolated.
- Use
executeOperationfor integration tests that cover the schema, validation, and resolvers together without an HTTP server. - Pass mock databases through context to keep tests fast and environment-independent.
- Test error cases explicitly — unauthorized access, missing records, and validation failures.
