Part 10. Testing: Backend Testing - Unit Testing - Controllers

This post is part of a Series of post which I'm describing a clock-in/out system if you want to read more you can read the following posts:

Introduction

This is the first post about testing and can be the first post about Quality Assessment (QA). This project has not been developed using Test Drive-Development (TDD) from the beginning but I am currently doing the testing phase. Thanks to the testing phase I have identified a lot of mini-bugs which could had been a big problem in case this project had been in production. The reality is that the project will be in production mode in the following weeks. These tests will be very useful to repair several bugs that have been discovered in this time.

The first step to test is deciding what should I test? Anybody could say to you that you must testing the whole app and you must obtain a coverage near to 100% but the really is that you do not need to test the entire app but that you have to test the most critical parts of your software. These parts of your software could be a value near to 90% or 70% depending on your app.

In our case, I am going to describe that we should test:

  • Services:
    • app.service.
    • user.service.
    • auth.service.
  • Controllers:
    • app.controller.
    • user.controller.

Therefore, in our project, there is no need to do test DTOs, constants, entities and modules because those test are hard and the value is small.

The backend is developed using the NestJS framework which uses Jest as testing tool. Furthermore, NestJS includes a powerful package to testing which emulate an environment similar to the Angular Testing Package.

Controllers Testing

In this post, I'm going to describe the controllers unit test. This test are the most simple test in the test pyramid after of services unit test. My recommendation to the starters in the testing world is that you start unit test of the services because these are small functions which have an unique task and are easily isolated. Then, the next step in the testing world is that you doing controllers unit test because the controllers frequently invoke services methods.

App Controller

The first controller we are going to test is the app.controller.ts which use a service: AppService. Therefore, our test suite must check that app.service will invoke the methods using the correct parameters.

The first step consists of the initial configuration for each test that we will develop. So, the app.controller.ts requires a service in its constructor (AppService) which will be a spy. The Test package from @nestjs/testing provides the method createTestingModule which creates a testing module to test. In this testingModule the providers array is composed by the AppController and a spy created using a factory to intercept the AppService. The following code shows you this initial configuration:

describe('App Controller', () => {
  let testingModule: TestingModule;
  let controller: AppController;
  let spyService: AppService;

  beforeEach(async () => {
    testingModule = await Test.createTestingModule({
      controllers: [AppController],
      providers: [
        {
          provide: AppService,
          useFactory: () => ({
            authIn: jest.fn(() => true),
            authOut: jest.fn(() => true),
            usersTicketing: jest.fn(() => true),
          }),
        },
      ],
    }).compile();
    controller = testingModule.get(AppController);
    spyService = testingModule.get(AppService);
  });
  
  ...
  

The next step consits of knowing what we want to test. The main idea is to test each function/method independently of any other. So, the following methods are the code of app.controller.ts.

@Post('in')
authIn(@Body() ticket: AuthDto): Promise<AuthResponseDto> {
  return this.appService.authIn(ticket);
}

@Post('out')
authOut(@Body() ticket: AuthDto): Promise<AuthResponseDto> {
  return this.appService.authOut(ticket);
}
@Get('users')
usersTicketing(): Promise<{ users: User[]; timestamp: number }> {
  return this.appService.usersTicketing();
}

The authIn, authOut and ``ùsersTicketingmethods should check that theappService``` is invoke using the correct parameters. In our case, the test is unit and, therefore, these methods should not be invoke using the real function/method, that is the reason why we are using a spy for these methods. The code to test the functions is the following one:

describe('authIn', () => {
   it('should authIn an user', async () => {
     const params: AuthDto = {
       key: 'key',
       reader: 'reader',
     };
     controller.authIn(params);
     expect(spyService.authIn).toHaveBeenCalledWith(params);
   });
 });

 describe('authOut', () => {
   it('should authOut an user', async () => {
     const params: AuthDto = {
       key: 'key',
       reader: 'reader',
     };
     controller.authOut(params);
     expect(spyService.authOut).toHaveBeenCalledWith(params);
   });
 });

 describe('usersTicketing', () => {
   it("should return the user's list who clock-in", async () => {
     controller.usersTicketing();
     expect(spyService.usersTicketing).toHaveBeenCalled();
   });
 });

In the previous tests you can note that the expect is related with the method authIn, authOut and usersTicketing which check that these methods were invoke and the parameters were the corrects ones. In these methods the errors thrown in the methods authIn or authOut are not relevant due to in theses methods the responsibility is delegated to the AppService.

User Controller

The procedure to test the user controller is the same that was used in app.controller.ts. So, the first step is the create the test module which contains the spy and controller that will be used in the following test.

import { Test } from '@nestjs/testing';
import { TestingModule } from '@nestjs/testing/testing-module';
import { UserService } from '../services/users.service';
import { UserController } from './user.controller';

describe('User Controller', () => {
  let testingModule: TestingModule;
  let controller: UserController;
  let spyService: UserService;

  beforeEach(async () => {
    testingModule = await Test.createTestingModule({
      controllers: [UserController],
      providers: [
        {
          provide: UserService,
          useFactory: () => ({
            getUsersWithoutKey: jest.fn(() => true),
            addUser: jest.fn(() => true),
          }),
        },
      ],
    }).compile();
    controller = testingModule.get(UserController);
    spyService = testingModule.get(UserService);
  });
  
  ...
  

The methods are very easy to testing because the technique used is the same that in the app.controller.ts. So, the code to test is the following:

@Get()
getUsers(): Promise<User[]> {
  return this.userService.getUsersWithoutKey();
}

@Post()
addUser(@Body() userDto: { uid: string; key: string }): Promise<User> {
  return this.userService.addUser(userDto);
}

And its test suite only check if the methods getUsersWithoutKey and addUser are invoked with the correct parameters as you can see in the following code:

describe('getUser', () => {
  it('should return users withoutKey', async () => {
    controller.getUsers();
    expect(spyService.getUsersWithoutKey).toHaveBeenCalled();
  });
});

describe('addUser', () => {
  it('should add key to the user', async () => {
    const params = {
      uid: 'uid',
      key: 'string',
    }
    controller.addUser(params);
    expect(spyService.addUser).toHaveBeenCalledWith(params);
  });
});

Conclusion

In this post I have explained how you can test controllers of your backend using jest and the NestJS Framework. These test are very easy if you know as the spies works. In fact, the controllers frequently invoke methods of the services.

In the next post, I will show you as you can to do e2e test of the backend.