41 Introduction to Unit Testing for Resolvers
41 Introduction to Unit Testing for Resolvers
Unit testing has become a fundamental part of the modern software development lifecycle. It not only ensures that code behaves as expected, but also helps maintain code quality as the system grows and requirements change. In this 41st article of our introductory testing series, I’ll focus specifically on unit testing for resolvers—a component that is often overlooked in testing, especially in applications that use GraphQL or the resolver design pattern in traditional backends.
This article covers everything from what a resolver is, why a unit test matters, all the way to a complete implementation example with simulations, tables, and a flow diagram using mermaid.
What Is a Resolver?
Before we dive into unit testing, it’s important to understand what a resolver is.
As an illustration, in GraphQL a resolver usually looks like this:
1const resolvers = {
2 Query: {
3 user: (parent, args, context) => {
4 // Logic to fetch the user based on args.id
5 return getUserById(args.id);
6 },
7 },
8};Why Is Unit Testing Resolvers Important?
The following table summarizes why it’s important to unit test resolvers:
| Reason | Explanation |
|---|---|
| Isolating business logic | Ensures every function can run independently. |
| Catching errors earlier | Identifies bugs before the integration/deployment stage. |
| Preventing regressions | Reduces the risk of errors when the code is updated. |
| Documenting edge cases | Edge cases are documented through their tests. |
| Team/developer confidence | Unit tests become a reliable contract. |
Standard Resolver Structure
Let’s look at the following resolver template, common in Node.js + GraphQL projects:
1// resolvers/user.js
2
3const getUserById = require('../services/userService').getUserById;
4
5const userResolver = async (_, { id }, context) => {
6 // Validate input
7 if (!id) throw new Error('User ID is required');
8 // Fetch the user data
9 const user = await getUserById(id);
10 if (!user) throw new Error('User not found');
11 return user;
12};
13
14module.exports = userResolver;In the resolver above, we need to ensure three things:
- The ID input must be present.
- The
getUserByIdfunction is called and returns the correct result. - An error is thrown if the user is not found.
Unit Testing a Resolver: Scenario Simulation
To perform unit testing, we must isolate external dependencies such as the database/external services using mocking.
Tools Used
File Layout
1/services
2 userService.js
3/resolvers
4 user.js
5/tests
6 userResolver.test.jsUnit Test Example
1// tests/userResolver.test.js
2
3const userResolver = require('../resolvers/user');
4const userService = require('../services/userService');
5
6jest.mock('../services/userService');
7
8describe('User Resolver Unit Test', () => {
9 afterEach(() => {
10 jest.clearAllMocks();
11 });
12
13 it('should throw error if id is missing', async () => {
14 await expect(userResolver(null, {}, {})).rejects.toThrow('User ID is required');
15 });
16
17 it('should call getUserById with correct id', async () => {
18 userService.getUserById.mockResolvedValue({ id: '123', name: 'Andy' });
19 const result = await userResolver(null, { id: '123' }, {});
20 expect(userService.getUserById).toBeCalledWith('123');
21 expect(result).toEqual({ id: '123', name: 'Andy' });
22 });
23
24 it('should throw error if user not found', async () => {
25 userService.getUserById.mockResolvedValue(null);
26 await expect(userResolver(null, { id: '321' }, {})).rejects.toThrow('User not found');
27 });
28});Resolver and Testing Flow Diagram
To make things clearer, here is a simple flow of a resolver and its testing in a mermaid diagram:
flowchart TD
A[Mulai Unit Test] --> B{ID Diberikan?}
B -- Tidak --> C[Tolak dengan Error: ID is required]
B -- Ya --> D[Panggil getUserById(id)]
D --> E{User ditemukan?}
E -- Tidak --> F[Tolak dengan Error: User not found]
E -- Ya --> G[Kembalikan User]
Simulating the Unit Test Run
Let’s simulate the testing process in the following order:
- Test 1: No ID argument.
- The resolver immediately validates and throws an error.
- Test 2: The ID argument is provided and the user is found.
- The mocked service returns dummy data, and the resolver returns the result.
- Test 3: The ID argument is provided but the user is
null.- The mock returns null, so the resolver throws an error.
| No. | Scenario | Input | Mock Output | Expected Result |
|---|---|---|---|---|
| 1 | ID not provided | {} | - | Error: ID is required |
| 2 | User found | {id: '123'} | {id:'123', name} | User Object |
| 3 | User not found | {id: '321'} | null | Error: User not found |
Best Practices for Unit Testing Resolvers
- Test every path: Both the happy path and the error path.
- Mock external dependencies: Don’t let tests call the real service/database.
- Combine with coverage: Make sure critical paths are tested.
- Make tests readable: Test descriptions should be as clear as a specification.
- Don’t write integration tests here: A unit test only exercises a single unit/container.
Conclusion
Unit testing resolvers is often underrated, especially because a resolver feels like a simple function that’s “just a middleman.” Yet, as we’ve seen, unit testing resolvers is crucial. It guards the integration between domain logic, input validation, error handling, and the reliability of the response delivered to the frontend/API client.
By following best practices and leveraging tools like Jest, we can maintain code quality while making future changes more comfortable. I hope the explanations, examples, simulations, and tables in this 41st article give you enough insight to start testing resolvers in your own projects!
The writing and diagrams above are part of the Modern Testing Series on Medium. Don’t miss the next article covering more advanced testing techniques!