Unit Testing with NestJS and Jest: A comprehensive tutorial

Jack Allcock
Jul 22 · 9 min read

Introduction

Testing in NestJS has proved to be tricky due to the lack of documentation that surrounds it, however I think I have now cracked it. I decided to create this article to attempt to plug this gap of documentation.

It’s not that Unit testing is hard; it isn’t. I actually find it quite satisfying to almost ‘penetrate test’ other peoples code by thoroughly testing all events and watching all the tests light up with green ticks.

The only hard part I found was getting it to work in NestJS, like many other other server side frameworks such as Spring, due to a reliance on dependency injection and Controller-Service architecture. Isolating these took some work.

I have used the Jest framework to create these Unit tests.

What is a Unit Test?

A unit test is built upon the foundation of SOLID and the Single Responsibility Principle. As a result, you focus on testing in isolation. This means testing single parts of the application or very specific pieces of logic. Testing here is not interested in seeing how parts come together or if an entire API for example functions as expected. That would be better suited to an integration or end-to-end test.

As such unit testing is dependant on the fact that code is low coupled and cohesive.

A Basic Example

It may be best to start with a simple example to help those who have never looked at Jest testing before. Whilst its unique, Jest unit testing looks very similar to most other testing frameworks such as JUnit in Java so the skills are transferable.

describe("* Testing Maths", () => {
it("should return a number of 5", () => {
const x = 5;
const y = 0;
const expectedResult = 5;
expect(x + y).toEqual(expectedResult);
expect(typeof (x + y)).toBe("number");
});
});

As shown in the test above:

  • Describe — ‘describe’ illustrates in the terminal the general name of the test(s), as you can have multiple ‘it’ functions.
  • It — an ‘it’ function is a test and should have a description on what it should do/return.
  • Expect — ‘expect’ is a method that informs the test that this is what should happen. It will act as a Boolean though is a void method and fail if the comparison fails. You can have multiple of these in one test.
  • ToEqual — ‘toEqual’ will check if two values are the same and is one of many possible testing methods Jest takes advantage of. You can see more here.
  • There is no type checking Jest assertion, but this can be done with JavaScript’s native method ‘typeof’ and Jests ‘toBe’.

I recommended laying the test out exactly like this including the expected result variable. It makes the test easier to read and understand. When a test fails you need to work out why. By following this pattern you can easily see what happens and what it should return. It is also easier to change this way.

Mocking

Unfortunately most tests are not as simple and straight forward as that bit of maths.

A typical server design pattern follows that of controller, service, repository. Each should do a job independently of one another. As such, we should test each of them one at a time. However this is tricky because they are still dependant on one another due to dependency injection. If a controller injects a service how do we test the controller without testing the service as a result? This is Unit testing, we only want to test one.

We can do this with the very familiar testing principle of mocking.

This took me a while to get my head around so I expect you will find it tricky also. But the more you do it the more it clicks. Let’s take an example, the Client Module.

The process of mocking differs slightly between controllers and services, the latter is more tricky.

Tests for a Controller by mocking the services

If you look in the client controller, it has 2 dependencies. A client and firebase service. We can mock these at the top of our testing file like so:

jest.mock("../client/client.service");
jest.mock("../../core/firebase/firebase.service");

We have to include the absolute path, I know a pain right. The above will inform our test compiler that when the Client module is created and a service method is called, do not dependency inject them. Instead, expect a mocked implementation/result (this we will create).

We will wrap all the tests and setup in a describe method that will help us see in the terminal which tests these belong to:

describe("-- Client Controller --", () => {
...

Setting up the module

Before we start testing we have to create the module. Obviously we have to do this as we are not running a main input file (main.ts or app-modules.ts) which would normally create the modules. We use Nests testing library to create a testing module:

Specify the services, controller and module as class variables so that they can used in the whole scope of the suite. (This file is referred to as a suite).

import { Test, TestingModule } from "@nestjs/testing";describe("-- Client Controller --", () => {
let clientService: ClientService;
let firebaseService: FirebaseService;
let module: TestingModule;
let clientController: ClientController;
beforeAll(async () => {
module = await Test.createTestingModule({
controllers: [ClientController],
providers: [
ClientService,
FirebaseService,
]
}).compile();
clientService = module.get<ClientService>(ClientService);
firebaseService = module.get<FirebaseService>(FirebaseService);
clientController = module.get(ClientController);
});
afterEach(() => {
jest.resetAllMocks();
});

We create the module in the ‘beforeAll’ method. This will ensure that the module is created before any test can run. Just replicate what is in the module file (in this case, client.module) by passing in the Controller and Services.

In the ‘afterEach’ method, I reset all mocks so that each test starts afresh.

Let’s now create some tests:

describe("* Find One By Id", () => { it("should return an entity of client if successful", async () => {
const expectedResult = new ClientEntity();
const mockNumberToSatisfyParameters = 0;
jest.spyOn(clientService, "findOneById").mockResolvedValue(expectedResult);
expect(await clientController.findOneById(mockNumberToSatisfyParameters)).toBe(expectedResult);
});
it("should throw NotFoundException if client not found", async (done) => {
const expectedResult = undefined;
const mockNumberToSatisfyParameters = 0;
jest.spyOn(clientService, "findOneById").mockResolvedValue(expectedResult);
await clientController.findOneById(mockNumberToSatisfyParameters)
.then(() => done.fail("Client controller should return NotFoundException error of 404 but did not"))
.catch((error) => {
expect(error.status).toBe(404);
expect(error.message).toMatchObject({error: "Not Found", statusCode: 404});
done();
});
});
});

So the above test will test ‘finding a client by an id’. The two tests will check the controller API returns a client or a 404.

The find one by id method in the Client controller calls the client service ‘findOneById’ method. We are not testing this service. As a result we take the ‘Veil of Ignorance’ approach. This is the veil the controller itself should always wear. It shouldn’t care what the service does or responds, just what to do with it.

So we mock the service method. We do this with ‘jest.spyOn’. The method to mock goes in the double quotes. The method .mockResolvedValue is used because this service is asynchronous and returns a promise. This method will mock that the service returns a Client entity or in the second tests case, undefined.

In the first test we then call the controller method (which will use the mocked service). We then test that the controller returns this correctly.

In the next test we use promises because we are error handling. If there is no client, our controller returns an exception. If .then() is called then the client returned a valid response. If it does this then the test should fail. We know that the controller should return an exception. In the .catch() we check that the response is a 404 and it’s corresponding message with .toMatchObject.

Take a look at the Create Client test in client.controller.spec and see how we can spy on different service methods to change the controllers response:

As shown in the controller, it returns 3 exceptions if the results of 3 services are invalid:

/**
* Create Client
* @param id
* @param {CreateClientDto} createData
* @returns ClientEntity
*/
@Post()
@UseGuards(SystemUserGuard)
@UsePipes(ValidationPipe)
async create(@Body() createData: CreateClientDto): Promise<ClientEntity> {
const isValidClientId = await this.clientService.findOneById(createData.client_id);
const isValidClientType = await this.clientService.validClientType(createData.client_type);
const isValidFirebaseApp = await this.firebaseService.validFirebaseApp(createData.firebase_service_account_id);

if (isValidClientId !== undefined) {
throw new ConflictException("Already exists a client with the same client_id");
} else if (isValidClientType === false) {
throw new BadRequestException("Invalid client type");
} else if (isValidFirebaseApp === false) {
throw new BadRequestException("Invalid firebase account id");
}
return await this.clientService.create(createData);
}

We can use different tests to mock any of the 3 services in this controller to change the result. I.e we can mock the firebaseService.validFirebaseApp method to return false. Study the test below:

describe("* Create Client ", () => {
const dto = new CreateClientDto();
it("should return an object of client entity when created", async () => {
const expectedResult = new ClientEntity();
jest.spyOn(clientService, "create").mockResolvedValue(expectedResult);
expect(await clientController.create(dto)).toBe(expectedResult);
});
it("should return conflict if client already exists ", async (done) => {
const serviceMockResult = new ClientEntity();
// Pretend that a client does already exist
jest.spyOn(clientService, "findOneById").mockResolvedValue(serviceMockResult);
await clientController.create(dto)
.then(() => done.fail("Client controller shuold return conflict error of 409 but did not"))
.catch((error) => {
expect(error.status).toBe(409);
expect(error.message).toBe("Already exists a client with the same client_id");
done();
});
});

it("should return BadRequestException if invalid client type ", async (done) => {
const serviceMockResult = false;
jest.spyOn(clientService, "validClientType").mockResolvedValue(serviceMockResult);
await clientController.create(dto)
.then(() => done.fail("Client controller should return BadRequestException error of 400 but did not"))
.catch((error) => {
expect(error.status).toBe(400);
expect(error.message).toBe("Invalid client type");
done();
});
});
it("should return BadRequestException if invalid firebase account id", async (done) => {
const serviceMockResult = false;
jest.spyOn(firebaseService, "validFirebaseApp").mockResolvedValue(serviceMockResult);
await clientController.create(dto)
.then(() => done.fail("Client controller should return BadRequestException error of 400 but did not"))
.catch((error) => {
expect(error.status).toBe(400);
expect(error.message).toBe("Invalid firebase account id");
done();
});
});
});

Tests for a service by mocking the repositories

In our services you may or may not need to mock a repository. This depends on if the service you are testing accesses the database or just performs some logic. We do not want to test the database. We again take the ‘Veil of Ignorance’ approach. Whatever that database returns we don’t care, just handle it and do the job. Module setup is a bit easier here as we do not need to reset any mocks. Setup like so:

describe("-- Client Service --", () => {
let clientService: ClientService;
let module: TestingModule;
let clientRepositoryMock: MockType<Repository<ClientEntity>>;
let clientTypeRepositoryMock: MockType<Repository<ClientTypeEntity>>;
const mockNumberToSatisfyParameters = 0;
beforeAll(async () => {
module = await Test.createTestingModule({
providers: [
ClientService,
{ provide: getRepositoryToken(ClientEntity), useFactory: repositoryMockFactory },
{ provide: getRepositoryToken(ClientTypeEntity), useFactory: repositoryMockFactory },
]
}).compile();
clientService = module.get<ClientService>(ClientService);
clientRepositoryMock = module.get(getRepositoryToken(ClientEntity));
clientTypeRepositoryMock = module.get(getRepositoryToken(ClientTypeEntity));
});

The biggest differences from the controller is the mocking of repositories. To do this we need to create custom mocks with Jest. We need to create the MockType type and the mock factory:

// @ts-ignore
export const repositoryMockFactory: () => MockType<Repository<any>> = jest.fn(() => ({
findOne: jest.fn(),
find: jest.fn(),
update: jest.fn(),
save: jest.fn()
}));
export type MockType<T> = {
[P in keyof T]: jest.Mock<{}>;
};

Just copy and paste this into the bottom of your testing file. The highlights show methods that I have added to the mock repository. These methods mock the methods that the actual repository should perform. As you go, just add them in.

Let’s create some tests using this mock repo:

The service we are testing:

/**
* Checks if the client is active (not deleted and not pending)
* @param {number} clientId
* @returns {boolean} isActive
*/
async isActive(clientId: number): Promise<boolean> {
const client = await this.findOne(clientId);
return client !== undefined && client.deleted === 0 && client.pending === 0 ? true : false;
}

We need to mock the repository this uses:

describe("* Client IsActive", () => {
it("should return true if active ", async () => {
const clientId = 1000;
const client = new ClientEntity();
client.clientId = clientId;
client.deleted = 0;
client.pending = 0;
clientRepositoryMock.findOne.mockReturnValue(client);
expect(await clientService.isActive(clientId)).toEqual(true);
});
it("should return false if client undefined ", async () => {
const clientId = 1000;
clientRepositoryMock.findOne.mockImplementation(undefined);
expect(await clientService.isActive(clientId)).toEqual(false);
});
it("should return false if client deleted and pending ", async () => {
const clientId = 1000;
const client = new ClientEntity();
client.clientId = clientId;
client.deleted = 1;
client.pending = 1;
clientRepositoryMock.findOne.mockReturnValue(client);
expect(await clientService.isActive(clientId)).toEqual(false);
});
});

As you can see in the highlights, we can mock what these repositories should return using ‘mockImplementation’ or ‘mockReturnValue’.

If you see the last test we see how the service should return false because we mock that the repository is going to return an entity which has been deleted or set to pending. This is the clear benefit over to end to end tests, we can test all possible outcomes.

Running tests

You can run tests with the — watch flag to automatically run tests with changes to code:

Press ‘a’ when asked to run all tests otherwise it will only run tests since the last commit.

npm run test:watch

Or you can run the default command that also shows you coverage:

npm run test

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade