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:
- email: string
- password: string
- 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 aservice
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:
- We instantiate an existing class from NestJS that implements the
ExecutionContext
interface: ExecutionContextHost. - 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. - 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:
- Mocking a complex interface (
ExecutionContext
) that is a required parameter to some function (even though it wasn't directly used); - 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 Advanced Concepts Course now LIVE!
- NestJS Authentication / Authorization Course now LIVE!
- NestJS GraphQL Course (code-first & schema-first approaches) are now LIVE!
- NestJS Authentication / Authorization Course now LIVE!