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 executeOperation for 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.

Leave a Comment