Skip to content

GraphQL Testing — Unit, Integration and E2E Testing Strategies

DodaTech Updated 2026-06-28 7 min read

In this tutorial, you will learn about GraphQL Testing. We cover key concepts, practical examples, and best practices to help you master this topic.

GraphQL testing requires strategies for unit testing resolvers, integration testing the full query pipeline, and end-to-end testing through the HTTP layer.

What You'll Learn

You will learn how to unit test resolvers with mocked context, integration test with Apollo Server, use test fixtures, snapshot test schemas, and set up CI testing.

Why Testing Matters

Untested GraphQL resolvers break silently — a resolver that returns null for a non-null field causes the entire parent to disappear. DodaTech's Durga Antivirus Pro team runs 500+ resolver unit tests, 100+ integration tests, and 20+ E2E tests per deployment, catching regressions before they reach production.

flowchart TB
    A["Testing Pyramid\nfor GraphQL"] --> B["Unit Tests\n(resolver logic)"]
    A --> C["Integration Tests\n(server + DB)"]
    A --> D["E2E Tests\n(full HTTP)"]
    B --> E["Jest + mock context"]
    C --> F["ApolloServer.executeOperation"]
    D --> G["supertest + HTTP"]
    style A fill:#dbeafe,stroke:#2563eb
    style B fill:#bbf7d0,stroke:#16a34a
    style C fill:#fef3c7,stroke:#d97706
    style D fill:#fca5a5,stroke:#dc2626
â„šī¸ Info

Prerequisites: GraphQL resolvers and schema. Jest testing framework.

Unit Testing Resolvers

// resolver.js
const resolvers = {
  Query: {
    threats: (_, { severity }, context) => {
      let results = context.db.threats;
      if (severity) {
        results = results.filter(t => t.severity === severity);
      }
      return results;
    },
  },
};

// resolver.test.js
describe('Query.threats', () => {
  it('returns all threats when no severity filter', () => {
    const mockContext = {
      db: {
        threats: [
          { id: '1', name: 'Emotet', severity: 'CRITICAL' },
          { id: '2', name: 'Adware', severity: 'LOW' },
        ],
      },
    };
    
    const result = resolvers.Query.threats(null, {}, mockContext);
    
    expect(result).toHaveLength(2);
    expect(result[0].name).toBe('Emotet');
  });
  
  it('filters threats by severity', () => {
    const mockContext = {
      db: {
        threats: [
          { id: '1', name: 'Emotet', severity: 'CRITICAL' },
          { id: '2', name: 'Adware', severity: 'LOW' },
        ],
      },
    };
    
    const result = resolvers.Query.threats(
      null, 
      { severity: 'CRITICAL' }, 
      mockContext
    );
    
    expect(result).toHaveLength(1);
    expect(result[0].name).toBe('Emotet');
  });
  
  it('handles empty results', () => {
    const mockContext = { db: { threats: [] } };
    const result = resolvers.Query.threats(null, {}, mockContext);
    expect(result).toEqual([]);
  });
  
  it('throws AuthenticationError for unauthorized access', () => {
    const mockContext = { db: { threats: [] }, user: null };
    const protectedResolver = resolvers.Query.myDevices;
    
    expect(() => {
      protectedResolver(null, {}, mockContext);
    }).toThrow(AuthenticationError);
  });
});

Integration Testing with Apollo Server

// integration test
const { ApolloServer, gql } = require('apollo-server');
const { createTestClient } = require('apollo-server-testing');

const typeDefs = gql`
  type Threat { id: ID! name: String! severity: String! }
  type Query { threats(severity: String): [Threat!]! }
`;

const resolvers = {
  Query: {
    threats: (_, { severity }) => {
      let threats = [
        { id: '1', name: 'Emotet', severity: 'CRITICAL' },
        { id: '2', name: 'Adware', severity: 'LOW' },
      ];
      if (severity) {
        threats = threats.filter(t => t.severity === severity);
      }
      return threats;
    },
  },
};

describe('Integration: threats query', () => {
  let server, client;
  
  beforeEach(() => {
    server = new ApolloServer({ typeDefs, resolvers });
    client = createTestClient(server);
  });
  
  it('returns all threats', async () => {
    const { data, errors } = await client.query({
      query: gql`query { threats { id name severity } }`,
    });
    
    expect(errors).toBeUndefined();
    expect(data.threats).toHaveLength(2);
  });
  
  it('filters by severity', async () => {
    const { data } = await client.query({
      query: gql`query GetCritical($severity: String!) {
        threats(severity: $severity) { id name }
      }`,
      variables: { severity: 'CRITICAL' },
    });
    
    expect(data.threats).toHaveLength(1);
    expect(data.threats[0].name).toBe('Emotet');
  });
  
  it('rejects invalid queries', async () => {
    const { errors } = await client.query({
      query: gql`query { invalidField }`,
    });
    
    expect(errors).toHaveLength(1);
    expect(errors[0].message).toContain('Cannot query field');
  });
});

E2E Testing with Supertest

const request = require('supertest');
const { ApolloServer, gql } = require('apollo-server-express');
const express = require('express');

describe('E2E: GraphQL HTTP endpoint', () => {
  let app, server;
  
  beforeAll(async () => {
    app = express();
    server = new ApolloServer({
      typeDefs,
      resolvers,
      context: ({ req }) => ({ user: req.user }),
    });
    
    await server.start();
    server.applyMiddleware({ app });
  });
  
  afterAll(async () => {
    await server.stop();
  });
  
  it('responds to valid GraphQL queries', async () => {
    const response = await request(app)
      .post('/graphql')
      .send({ query: '{ threats { id name } }' });
    
    expect(response.status).toBe(200);
    expect(response.body.data.threats).toBeDefined();
  });
  
  it('rejects malformed queries with 400', async () => {
    const response = await request(app)
      .post('/graphql')
      .send({ query: '{ invalid syntax' });
    
    expect(response.status).toBe(400);
  });
  
  it('handles authentication headers', async () => {
    const response = await request(app)
      .post('/graphql')
      .set('Authorization', 'Bearer valid-token')
      .send({ query: '{ myDevices { id name } }' });
    
    expect(response.status).toBe(200);
  });
  
  it('enforces rate limiting', async () => {
    const requests = Array(101).fill().map(() =>
      request(app).post('/graphql').send({ query: '{ threats { id } }' })
    );
    
    const responses = await Promise.all(requests);
    const rateLimited = responses.filter(r => r.status === 429);
    expect(rateLimited.length).toBeGreaterThan(0);
  });
});

Snapshot Testing Schemas

// schema snapshot test
const { printSchema } = require('graphql');
const { makeExecutableSchema } = require('@graphql-tools/schema');

describe('GraphQL Schema', () => {
  it('matches the stored snapshot', () => {
    const schema = makeExecutableSchema({ typeDefs, resolvers });
    const printedSchema = printSchema(schema);
    expect(printedSchema).toMatchSnapshot();
  });
  
  it('has required Query fields', () => {
    const schema = makeExecutableSchema({ typeDefs, resolvers });
    const queryType = schema.getQueryType();
    
    expect(queryType.getFields()).toHaveProperty('threats');
    expect(queryType.getFields()).toHaveProperty('devices');
    expect(queryType.getFields()).toHaveProperty('user');
  });
  
  it('validates schema without errors', () => {
    const schema = makeExecutableSchema({ typeDefs, resolvers });
    const errors = validateSchema(schema);
    expect(errors).toHaveLength(0);
  });
});

Mocking Data Sources

// Mock DataLoader for resolver tests
function createMockLoader() {
  const batchFn = jest.fn((keys) => {
    return Promise.resolve(keys.map(k => ({ id: k, name: `Mock-${k}` })));
  });
  
  return {
    loader: new DataLoader(batchFn),
    batchFn,
  };
}

describe('Device resolvers with DataLoader', () => {
  it('uses DataLoader for threats', async () => {
    const { loader, batchFn } = createMockLoader();
    const device = { id: 'dev-001', name: 'Office-PC' };
    
    const result = await resolvers.Device.threats(device, {}, {
      threatLoader: loader,
    });
    
    expect(batchFn).toHaveBeenCalledWith(['dev-001']);
    expect(result).toHaveLength(1);
    expect(result[0].id).toBe('dev-001');
  });
});

Common Mistakes

1. Not Testing Null Propagation

If a non-null resolver returns null, the entire parent becomes null. Test that every non-null resolver actually returns data.

2. Testing With Real Databases in Unit Tests

Unit tests should use mock data sources. Using a real database makes tests slow, flaky, and dependent on environment.

3. Forgetting to Test Error Cases

Testing the happy path is easy. Testing auth failures, validation errors, empty results, and network timeouts is more important — and often missed.

4. Asserting on Error Messages

Error messages change. Assert on error codes (extensions.code) instead of message strings.

5. Not Using Test Snapshots for Schema

Without schema snapshot tests, an accidental schema change (removing a field, changing nullability) goes unnoticed until clients break.

Practice Questions

  1. How do you unit test a GraphQL resolver?
  2. What is apollo-server-testing?
  3. How do you test authentication in resolvers?
  4. Why use snapshot tests for schemas?
  5. How do you mock DataLoader in tests?

Answers:

  1. Call the resolver function directly with mock parent, args, and context arguments. Assert on the return value and side effects.
  2. apollo-server-testing provides createTestClient(server) which returns query and mutate functions for testing queries against a real Apollo Server instance without HTTP.
  3. Set context.user to different values (null, viewer, admin) and assert that the resolver throws or returns appropriate data based on the user's role.
  4. Schema snapshot tests catch unintended changes to the API contract — removed fields, changed types, modified nullability — before they break clients.
  5. Use Jest's jest.fn() to create a mock batch function, pass it to a new DataLoader(mockFn), and assert that the loader was called with the expected keys.

Challenge: Build a comprehensive test suite for DodaTech's GraphQL API. Include unit tests for all resolvers (with mocked context and DataLoaders), integration tests for the full query pipeline (with createTestClient), E2E tests through HTTP (with supertest), schema snapshot tests, and test fixtures for mock data. Achieve 90%+ Code Coverage.

FAQ

Should I test the gateway or individual subgraphs?

Test both — unit test individual subgraph resolvers, integration test the gateway composition, and E2E test through the gateway endpoint.

How do I test subscription resolvers?

Subscribe in the test, trigger the event via a mutation, and assert that the subscription receives the expected payload. Use client.subscribe() in tests.

What is the best mocking strategy for resolvers?

Mock at the data source boundary — mock context.db and context.loader, not the resolver itself. This tests the resolver logic against controlled data.

How do I test query cost and depth limits?

Set low limits in test config, send queries that exceed them, and assert that validation rejects them. This tests your security configuration.

Can I use fixtures for test data?

Yes — create test/fixtures/ files with sample data (users, devices, threats). Import them in multiple test files for consistent, reusable test data.

Mini Project

Build a complete test suite for DodaTech's GraphQL API. Create test fixtures for all entity types. Write unit tests for every resolver (including error cases, auth checks, and edge cases). Write integration tests for all queries and mutations. Add E2E tests for the HTTP layer. Set up schema snapshot testing. Configure Jest with coverage reporting.

What's Next

Topic Description
Security Depth limiting, cost analysis
Performance Query optimization and Caching
Project Full-stack GraphQL application
âŦ… Code Generation
➡ Security

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro