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 the provide property of animalServiceFactory 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:

Image of the response from the Cat service
Image of the response from the Cat service

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:

Image of the response from the Dog service
Image of the response from the Dog service

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:

Image of bad test coverage in the app module file
Image of bad test coverage in the app module file

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 the REQUEST 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:

Image of good test coverage in the app module file
Image of good test coverage in the app module file

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