Testing Factory Classes in NestJS
Having recently worked on a NestJS project, I had to work out how to test a factory class as to maintain 100% code coverage in the codebase. After spending considerable time looking through tutorials/guides with no success, I had to throw myself into the deep end and figure it out myself.
I'll now show you how I managed to get this working and hopefully pull you out of the rabbit hole you might be in as I was when trying to solve the problem myself.
Let's get on with the following (very contrived) example.
Setup
Let's start by creating a project using the NestJS CLI. Follow the instructions explained in the official documentation. For the lazy people out there, here is what to do:
$ npm i -g @nestjs/cli
$ nest new project-name
Now run the development server:
$ npm run start:dev
Lets create a number of files for the sake of this example. Create the following files in the src/
directory:
src/animals/cat.service.ts
import { Injectable } from "@nestjs/common"
@Injectable()
export class CatService {
makeNoise(): string {
return "Meow!"
}
}
src/animals/dog.service.ts
import { Injectable } from "@nestjs/common"
@Injectable()
export class DogService {
makeNoise(): string {
return "Woof!"
}
}
Now modify app.module.ts
so that it looks like the following:
import { Module, Scope } from "@nestjs/common"
import { REQUEST } from "@nestjs/core"
import { Request } from "express"
import { CatService } from "./animals/cat.service"
import { DogService } from "./animals/dog.service"
import { AppController } from "./app.controller"
import { AppService } from "./app.service"
export const animalServiceFactory = {
provide: "ANIMAL",
scope: Scope.REQUEST,
useFactory: (req: Request): CatService | DogService => {
const { animal } = req.query
if (animal === "dog") {
return new DogService()
}
return new CatService()
},
inject: [REQUEST],
}
@Module({
imports: [],
controllers: [AppController],
providers: [animalServiceFactory, AppService],
})
export class AppModule {}
A few things to note here:
- We have created a factory (official documentation) that is bound to the incoming network request.
- We inject
REQUEST
into the factory so that we can peek into the incoming request object inside the factory function - We check the
animal
query parameter and return the respective class based on its value.
Finally, we need to inject the result of this dynamic service into the controller. Modify app.controller.ts
:
import { Controller, Get, Inject } from "@nestjs/common"
@Controller()
export class AppController {
constructor(@Inject("ANIMAL") private animal) {}
@Get()
getHello(): string {
return this.animal.makeNoise()
}
}
- We're passing in the token
"ANIMAL"
into the@Inject
decorator - this is what we defined in theprovide
property ofanimalServiceFactory
from the previous step.
Now let's go to the following address in the web browser: http://localhost:3000. We should see the text Meow!
in the document:
Let's now add our animal
param to the URL: http://localhost:3000/?animal=dog. We should now see the text Woof!
in the document:
The Approach
Now if we run npm run test:cov
and open /coverage/lcov-report/src/app.module.ts.html
in the web browser, we'll see this:
Let's get rid of those ugly red highlights. Create the following file:
src/app.module.spec.ts
import { ContextId, ContextIdFactory } from "@nestjs/core"
import { Test, TestingModule } from "@nestjs/testing"
import { CatService } from "./animals/cat.service"
import { DogService } from "./animals/dog.service"
import { animalServiceFactory } from "./app.module"
describe("AppModule", () => {
let app: TestingModule
let contextId: ContextId
beforeEach(async () => {
app = await Test.createTestingModule({
providers: [animalServiceFactory],
}).compile()
await app.init()
contextId = ContextIdFactory.create()
})
it("should return the cat service", async () => {
app.registerRequestByContextId(
{
query: {},
},
contextId
)
app.resolve("ANIMAL", contextId).then(provider => {
expect(provider).toBeInstanceOf(CatService)
})
})
it("should return the dog service", async () => {
app.registerRequestByContextId(
{
query: {
animal: "dog",
},
},
contextId
)
app.resolve("ANIMAL", contextId).then(provider => {
expect(provider).toBeInstanceOf(DogService)
})
})
})
- The initialisation of the
contextId
is the key here so that we are able to correctly (and manually) generate theREQUEST
object to be ultimately bound to our factory in this jest test. More information on this can be found here
Now re-run npm run test:cov
and reload the file /coverage/lcov-report/src/app.module.ts.html
in the web browser. We now see this:
Voila!
Conclusion
It wasn't an obvious (or even simple) implementation to make to correctly get Jest to play nicely with our factory, but now we can test our service properly and maintain strong test coverage in our codebase.
A well tested and covered codebase is a good codebase!
Comments