Testing Resolvers
Resolvers are the core of GraphQL execution. They bridge the schema with your application logic and data sources. Unit testing resolvers helps you validate the behavior of individual resolver functions in isolation, without needing to run GraphQL operations or construct a full schema.
Resolvers are good candidates for unit testing when they:
- Contain business logic
- Handle conditional behavior or error states
- Call external services or APIs
- Perform transformations or mappings
Unit tests for resolvers are fast, focused, and easy to debug. They help you verify logic at the function level before wiring everything together in integration tests.
Tip: Keep resolvers thin. Move heavy logic into service functions or utilities that can also be tested independently.
Setup
You don’t need a schema or GraphQL server to test resolvers. You just need a test runner and a function.
Test runners
You can use any JavaScript or TypeScript test runner. Two popular options are:
node:test
(built-in)- Native test runner in Node.js 18 and later
- Use
--experimental-strip-types
if you’re using TypeScript without a transpiler - Minimal setup
node --test --experimental-strip-types
- Jest
- Widely used across JavaScript applications
- Built-in support for mocks, timers, and coverage
- Better ecosystem support for mocking and configuration
Choose the runner that best fits your tooling and workflow. Both options support testing resolvers effectively.
Writing resolver tests
Unit tests for resolvers treat the resolver as a plain function.
You provide the args
, context
, and info
, then assert on the result.
Basic resolver function test
You do not need the GraphQL schema or graphql()
to write a basic resolver
function test, just call the resolver directly:
function getUser(_, { id }, context) {
return context.db.findUserById(id);
}
test('returns the expected user', async () => {
const context = {
db: { findUserById: jest.fn().mockResolvedValue({ id: '1', name: 'Alice' }) },
};
const result = await getUser(null, { id: '1' }, context);
expect(result).toEqual({ id: '1', name: 'Alice' });
});
Async resolvers and Promises
Always await
the result of async resolvers or return the Promise from your test:
test('resolves a user from async function', async () => {
const context = {
db: { find: async () => ({ name: 'Bob' }) },
};
const result = await resolver(null, {}, context);
expect(result.name).toBe('Bob');
});
Error handling
Resolvers often throw errors intentionally. Use expect(...).rejects
to
test these cases:
test('throws an error for missing user', async () => {
const context = {
db: { findUserById: jest.fn().mockResolvedValue(null) },
};
await expect(getUser(null, { id: 'missing' }, context))
.rejects
.toThrow('User not found');
});
Also consider testing custom error classes or error extensions.
Custom scalars
Custom scalars often include serialization, parsing, and validation logic. You can test them directly:
import { GraphQLDate } from '../scalars/date.js';
test('parses ISO string to Date', () => {
expect(GraphQLDate.parseValue('2023-10-10')).toEqual(new Date('2023-10-10'));
});
test('serializes Date to ISO string', () => {
expect(GraphQLDate.serialize(new Date('2023-10-10')))
.toBe('2023-10-10T00:00:00.000Z');
});
You can also test how your resolver behaves when working with scalar values:
test('returns a serialized date string', async () => {
const result = await getPost(null, { id: '1' }, {
db: {
findPostById: () => ({ createdAt: new Date('2023-10-10') }),
},
});
expect(result.createdAt).toBe('2023-10-10T00:00:00.000Z');
});
Best practices for unit testing resolvers
Use dependency injection
Resolvers often rely on a context
object. In tests, treat it as an injected
dependency, not a global.
Inject mock services, database clients, or loaders into context
. This pattern
makes resolvers easy to isolate and test:
const context = {
db: {
findUserById: jest.fn().mockResolvedValue(mockUser),
},
};
Mocking vs. real data
Use mocks to unit test logic without external systems. If you’re testing logic that depends on specific external behavior, use a stub or fake—but avoid hitting real services in unit tests. Unit tests should not make real database or API calls. Save real data for integration tests.
Testing resolver-level batching
If you use DataLoader
or a custom batching layer, test that batching works as
expected:
const userLoader = {
load: jest.fn().mockResolvedValue({ id: '1', name: 'Alice' }),
};
const context = { loaders: { user: userLoader } };
// Make multiple resolver calls
await Promise.all([
getUser(null, { id: '1' }, context),
getUser(null, { id: '1' }, context),
]);
expect(userLoader.load).toHaveBeenCalledTimes(1);
You can also simulate timing conditions by resolving batches manually or using fake timers in Jest.