Advanced Testing Strategies with Mocksin NestJS

Thiago Martins | Trilon Consulting
Thiago Martins

It is widely known how necessary tests are for reliability and ease of refactoring in any serious application. Quoting Uncle Bob:

"Tests are as crucial to the health of a project as the production code is." (Clean Code, Chapter 9: Unit Tests).

There are also many strategies and disciplines defining the processes and the structures of good test design like TDD (test-driven development) and BDD (behaviour-driven development), and certainly a lot of good articles explaining those patterns. Nonetheless, there aren't so many describing some nitty gritty - yet tricky - details that oppose our efforts as developers to create good tests. This post aims to tackle one such impediment: Dealing with hard-to-setup dependencies in unit tests (using NestJS of course ).

To show the tools we will use in this article, it is essential to understand an example of the problem we are trying to overcome. Imagine that we have an API to register user data. The client side has to send the following attributes to the [POST] /register endpoint:

  1. email: string
  2. password: string
  3. profilePic: string (base64)

Once these fields get into our back-end, we save them in some database. The response to the client side also has to be wrapped in a data attribute, like the following JSON:

{
“data”: {
“id”: 1,
“email”: “john@mail.com”
}
}

Our first discussion here will be about wrapping the response in a data attribute.

An easy way to have this wrapping for this endpoint and all others in our API would be to use an interceptor. This interceptor would, no surprise, intercept an endpoint's response and put it inside the data attribute. This is a reasonably easy implementation and one of the examples used in NestJS official docs:

export interface Response<T> {
data: T;
}
@Injectable()
export class WrapDataInterceptor<T> implements NestInterceptor<T, Response<T>> {
intercept(
context: ExecutionContext,
next: CallHandler
): Observable<Response<T>> {
return next.handle().pipe(map((data) => ({ data })));
}
}

Creating the Tests

At this point, as good software developers, we would usually think of creating a test for such an interceptor. If we are following TDD practices, then before creating the interceptor itself. However, If you have already tried to create a unit test for an Interceptor before, you know there is a catch: the ExecutionContext. In some cases, as in the code snippet above, we don't even need it for our purposes. Even so, we need to pass a context to this method, and since we are using TypeScript, and we typed it as an ExecutionContext we have to follow that interface.

Important sidenote: Even though we'll use a mocking strategy here, it's highly advisable to consider if you really want to test the interceptor class (or any other hard-to-test dependency). A good alternative is to use the Humble Object Pattern, which basically means to extract most part of the valuable rules into another class, with a simpler API that is easier to test. In this case, for instance, we could extract the wrapping logic to a service class and create tests for it. Nonetheless, there are some situations (mostly related to the project environment) when you have to test the class itself, so keep reading if that's your case.

The execution context is a wrapper around the request and some related metadata, providing a somewhat unified interface amongst different (web) contexts (HTTP services, microservices, Web Sockets). The problem is with the number of methods it has:

getArgs<T extends Array<any> = any[]>(): T;
getArgByIndex<T = any>(index: number): T;
switchToRpc(): RpcArgumentsHost;
switchToHttp(): HttpArgumentsHost;
switchToWs(): WsArgumentsHost;
getType<TContext extends string = ContextType>(): TContext;
getClass<T = any>(): Type<T>;
getHandler(): Function;

To test our simple interceptor, we need to build an object that implements all these methods and behaves as we need in our tests. There are 3 ways of doing this:

  1. We instantiate an existing class from NestJS that implements the ExecutionContext interface: ExecutionContextHost.
  2. We use a Test Double instead of the ExecutionContext. A Test Double is a proxy of some object, serving the same interface but with controlled behavior and state for test purposes.
  3. We could coerce the ExecutionContext type using a type workaround like { } as unknown as ExecutionContext or { } as never.

The problem with the first approach here is that we must understand and couple ourselves with the details of constructing an ExecutionContextHost. Having to pass the parameters correctly here seems a bit out of the scope of this test since this is all automatically handled by Nest during runtime.

The second approach, however, is also not so simple. If we were to create such a mock for this interface, using jest as our test runner, it would look something like this:

export interface ExecutionContextMock {
getType: jest.Mock<any, any>;
switchToHttp: jest.Mock<any, any>;
getRequest: jest.Mock<any, any>;
getClass: jest.Mock<any, any>;
getHandler: jest.Mock<any, any>;
getArgs: jest.Mock<any, any>;
getArgByIndex: jest.Mock<any, any>;
switchToRpc: jest.Mock<any, any>;
switchToWs: jest.Mock<any, any>;
}
/**
* creates a mock object for ExecutionContext
*/
export const buildExecutionContextMock = (): ExecutionContextMock => ({
getType: jest.fn(),
switchToHttp: jest.fn().mockReturnThis(),
getRequest: jest.fn(),
getClass: jest.fn(),
getHandler: jest.fn(),
getArgs: jest.fn(),
getArgByIndex: jest.fn(),
switchToRpc: jest.fn(),
switchToWs: jest.fn(),
});

With the code above, we could create a mock of the ExecutionContext simply with const executionContext = buildExecutionContextMock() and pass it to the pipe as a parameter. Since all methods also return jest mock functions, we can control their behavior for each test. The problem here, of course, is again the number of methods we have to manually declare and mock.

The third option could indeed work for the case where we don't really use the executionContext variable. However, if we need some information or behavior from it in the future, this won't work anymore. So, let's take a look at an exciting alternative.

createMock to the rescue

Now, we get to the exciting part of this article, the tool to save us from the pain of manual mocking: @golevelup/ts-jest. This package, supported by the community and one of NestJS's core team members, provides us with a clean and powerful method called createMock. With this method in hand, we only need to specify a type argument, and it will "auto-magically" create an object with all methods implemented as mocks. Check out the following example:

const mockExecutionContext = createMock<ExecutionContext>();
// Now, we can perform any operation, including nested ones, with this mock
// For instance, we can switch the context to http and get the request.
mockExecutionContext.switchToHttp().getRequest();

With that in hand, we can easily create a unit test for our interceptor:

describe('WrapDataInterceptor', () => {
const executionContext = createMock<ExecutionContext>();
const wrapDataInterceptor = new WrapDataInterceptor();
it("should wrap the next handler response in 'data' object", function (done) {
// Arrange
const someData = {
Id: 1,
email: 'john@mail.com',
};
// This guarantees our data will be returned to our interceptor
const callHandler = {
handle() {
return of(someData);
},
};
// Act
wrapDataInterceptor
.intercept(executionContext, callHandler)
.subscribe((asyncData) => {
// Assert
expect(asyncData).toEqual({
data: { ...someData },
});
done();
});
});
});

The comments separating the test in Arrange, Act and Assert blocks are a known pattern for test structuring called AAA. You can check it out here.

It's worth mentioning that we use the .subscribe method and assert the resulting value in its inner block because an Interceptor returns an RxJS Observable. But putting that aside, you can see how easy it was to mock the ExecutionContext. Moreover, the createMock utility can be used not only for this context, but if you have any dependency on your tests that are hard or weird to instantiate, this could prove to be a helpful alternative, as we'll see in the next section.

Unit Testing with more complex dependencies

Now, let's imagine we have a UserService responsible for, among other operations, creating a record of users, as we mentioned at the beginning of this post. Furthermore, it also has two other dependencies: EmailService, which is used to send an email when a user is registered, and S3BucketService, which is used to store the profile pictures sent in this endpoint:

export class UsersService {
constructor(
private readonly emailService: EmailService,
private readonly s3BucketService: S3BucketService
) {}
public async createUser(createUserDto: CreateUserDTO) {
// implementation here
}
}

If we want to create a unit test for this service, we must provide the emailService and the s3BucketService implementations. If these classes are from another module, we would also need to import them here:

module = await Test.createTestingModule({
imports: [EmailModule, BucketModule],
providers: [UsersService],
}).compile();
const usersService = await module.get(UsersService);

Now, If you think about the test scenario here, we don't want emails to be sent during tests, nor do we want to save profile pictures in actual "s3" buckets. For this reason, it feels weird to use the actual implementation, and then we have to provide an alternative one, a Test Double.

It would be nice to have an easy way to inject these services, especially when they have many methods to be mocked, right? To handle that, we can also use createMock to instantiate proxies for them!

module = await Test.createTestingModule({
providers: [
UsersService,
{ provide: EmailService, useValue: createMock<EmailService>() },
{ provide: S3BucketService, useValue: createMock<S3BucketService>() },
],
}).compile();
const usersService = await module.get(UsersService);

With the snippet above, our usersService will work correctly with its collaborators. Furthermore, we can test if the email was sent and also if the profile picture was saved by checking if the services' methods were called:

const emailService = module.get<EmailService>(EmailService);
expect(emailService.sendEmail).toHaveBeenCalled();
const s3BucketService = module.get<S3BucketService>(S3BucketService);
expect(s3BucketService.saveImage).toHaveBeenCalled();

Remember to use clearAllMocks in an afterEach block to prevent one test run to influence others.

Mocking many dependencies

As a final exercise, let's imagine our UsersService has many other dependencies: CacheService - Responsible for caching responses LogService - Responsible for creating and storing logs of called methods MetricsService - Responsible for collecting some metrics during runtime

It can become cumbersome to have so many custom providers using useValue: createMock<Interface>(). To solve this problem, NestJS V8 has a nice and shiny built-in feature called Auto Mocking. With the method useMocker, we can pass in a factory function that receives a token and returns a mock for a given token, allowing us to define the details of which tokens we want to mock and how we'll mock them:

module = await Test.createTestingModule({
providers: [UsersService],
})
.useMocker((token) => {
if (token === EmailService) {
return { sendEmail: jest.fn() };
}
if (token === S3BucketService) {
return { saveImage: jest.fn() };
}
// ...
})
.compile();
const usersService = await module.get(UsersService);

You may have noticed that this does not look simpler / easier than what we've been doing before. But the good news is that the createMock function is a factory function that receives a token as a parameter and returns a complete mock of said token. This is precisely what we need for all dependencies here, and we can simplify the code above:

module = await Test.createTestingModule({
providers: [UsersService],
})
.useMocker(createMock)
.compile();
const usersService = await module.get(UsersService);

With this new implementation, every missing dependency of our module will be mocked using the createMock function, and we're safe to unit test our methods without coupling and importing many other modules.

Conclusion and further reading

We've discussed the importance of testing our application and the difficulties of doing so. A challenge that frequently arises is how to deal with dependencies and collaborators of our system under test (SUT). There are some alternatives to it, from using the actual implementation to building malleable test doubles. Here, we saw the usage of createMock utility function to handle 2 common scenarios:

  1. Mocking a complex interface (ExecutionContext) that is a required parameter to some function (even though it wasn't directly used);
  2. Mocking dependencies that are hard to instantiate or weird to use in tests (i.e., EmailService).

We also used the autoMocker feature to facilitate mocking multiple dependencies, a handy tool if we don't want to rely on the importation of many modules and their actual implementations.

We barely scratched the surface of unit testing here. If you are interested in more details on handling class dependencies in tests, the different strategies, and their consequences to the software architecture, this article is a good starting point.


Learn NestJS - Official NestJS Courses 📚

Level-up your NestJS and Node.js ecosystem skills in these incremental workshop-style courses, from the NestJS Creator himself, and help support the NestJS framework! 🐈

🚀 The NestJS Fundamentals Course is now LIVE and 25% off for a limited time!

🎉 NEW - NestJS Course Extensions now live!
#NestJS
#NodeJS
#Tests
#Mocks

Share this Post!

📬 Trilon Newsletter

Stay up to date with all the latest Articles & News!

More from the Trilon Blog .

Jay McDoniel | Trilon Consulting
Jay McDoniel

NestJS Metadata Deep Dive

In this article we'll be doing a deep-dive and learning about how NestJS uses Metadata internally for everything from dependency injection, to decorators we use everyday!

Read More
Kamil Mysliwiec | Trilon Consulting
Kamil Mysliwiec

NestJS v10 is now available

Today I am excited to announce the official release of Nest 10: A progressive Node.js framework for building efficient and enterprise-grade, server-side applications.

Read More
Manuel Carballido | Trilon Consulting
Manuel Carballido

Implementing data source agnostic services with NestJS

Learn how to implement data source logic in an agnostic way in yours NestJS applications.

Read More

What we do at Trilon .

At Trilon, our goal is to help elevate teams - giving them the push they need to truly succeed in today's ever-changing tech world.

Trilon - Consulting

Consulting .

Let us help take your Application to the next level - planning the next big steps, reviewing architecture, and brainstorming with the team to ensure you achieve your most ambitious goals!

Trilon - Development and Team Augmentation

Development .

Trilon can become part of your development process, making sure that you're building enterprise-grade, scalable applications with best-practices in mind, all while getting things done better and faster!

Trilon - Workshops on NestJS, Node, and other modern JavaScript topics

Workshops .

Have a Trilon team member come to YOU! Get your team up to speed with guided workshops on a huge variety of topics. Modern NodeJS (or NestJS) development, JavaScript frameworks, Reactive Programming, or anything in between! We've got you covered.

Trilon - Open-source contributors

Open-source .

We love open-source because we love giving back to the community! We help maintain & contribute to some of the largest open-source projects, and hope to always share our knowledge with the world!

Explore more

Write us a message .

Let's talk about how Trilon can help your next project get to the next level.

Rather send us an email? Write to:

hello@trilon.io
© 2019-2023 Trilon.