GraphQL Testing â Unit, Integration and E2E Testing Strategies
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
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
- How do you unit test a GraphQL resolver?
- What is apollo-server-testing?
- How do you test authentication in resolvers?
- Why use snapshot tests for schemas?
- How do you mock DataLoader in tests?
Answers:
- Call the resolver function directly with mock
parent,args, andcontextarguments. Assert on the return value and side effects. apollo-server-testingprovidescreateTestClient(server)which returnsqueryandmutatefunctions for testing queries against a real Apollo Server instance without HTTP.- Set
context.userto different values (null, viewer, admin) and assert that the resolver throws or returns appropriate data based on the user's role. - Schema snapshot tests catch unintended changes to the API contract â removed fields, changed types, modified nullability â before they break clients.
- Use Jest's
jest.fn()to create a mock batch function, pass it to anew 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
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 |
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro