GraphQL

Testing GraphQL APIs

20 min Lesson 23 of 35

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:
  1. Unit tests for all query and mutation resolvers
  2. Integration tests for creating, updating, and deleting posts
  3. Tests for pagination and filtering
  4. Authentication and authorization tests
  5. Error handling tests for invalid input
  6. A snapshot test for the complete schema
Achieve at least 80% code coverage and document any edge cases.