Part 9. Testing: Backend Testing - Unit Testing - Services

NestJS NodeJS TypeScript

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 emule an environment similar to the Angular Testing Package.

Services Testing

In this post, I am going to describe the services unit test. This test are the most simple test in the test pyramid. My recomendation to the starters in the testing world is that you start unit testing the services because these are small functions which have an unique task and are easily isolated. For this reason, they are the most simple and easiest to test.

TestPyramid

App Service

The first service we are going to test is the app.service.ts which use two services: AuthService and UserService. Therefore, our test suite must check that app.service will invoke the services using the correct parameters.

The first step consists of the initial configuration for each test that we will develop. So, the app.service.ts requires two services in its constructor (AuthService and UserService) which will be spies. 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 AppService and two spies created using a factory. The following code shows you this initial configuration:

import { Test } from '@nestjs/testing';
import { TestingModule } from '@nestjs/testing/testing-module';
import { AppService } from './app.service';
import { AuthService } from './modules/auth/auth.service';
import { UserService } from './modules/users/services/users.service';
import { AuthDto } from './modules/auth/dto/auth.dto';

describe('App service', () => {
  let testingModule: TestingModule;
  let service: AppService;
  let spyAuthService: AuthService;
  let spyUserService: UserService;

  beforeEach(async () => {
    testingModule = await Test.createTestingModule({
      providers: [
        AppService,
        {
          provide: AuthService,
          useFactory: () => ({
            authIn: jest.fn(() => true),
            authOut: jest.fn(() => true),
          }),
        },
        {
          provide: UserService,
          useFactory: () => ({
            getUsersMustBeWorkingNow: jest.fn(() => true),
          }),
        },
      ],
    }).compile();

    service = testingModule.get(AppService);
    spyAuthService = testingModule.get(AuthService);
    spyUserService = testingModule.get(UserService);
  });
  
  ...
  

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 part of the code in app.service.ts.

public authIn(ticket: AuthDto): Promise<AuthResponseDto> {
    return this.authService.authIn(ticket);
  }
  public async authOut(ticket: AuthDto): Promise<AuthResponseDto> {
    return await this.authService.authOut(ticket);
  }
  public async usersTicketing(): Promise<{ users: User[]; timestamp: number }> {
    const usersMustBeWorking = await this.usersService.getUsersMustBeWorkingNow();
    return {
      users: usersMustBeWorking,
      timestamp: moment().unix(),
    };
  }

The authIn and authOut methods should check that the authService is invoked using the correct parameters. In our case, the test is unit and, therefore, the methods this.authService.authIn and this.authService.authOut should not be invoked using the real function/method, that is the reason why we are using spies for these methods. The code to test the functions is the following one:

 describe('authIn', () => {
    it('should invoke authIn from AuthService', async () => {
      const params: AuthDto = {
        key: 'key',
        reader: 'reader',
      };

      await service.authIn(params);

      expect(spyAuthService.authIn).toHaveBeenCalledWith(params);
    });
  });
  describe('authOut', () => {
    it('should invoke authOut from AuthService', async () => {
      const params: AuthDto = {
        key: 'key',
        reader: 'reader',
      };

      await service.authOut(params);

      expect(spyAuthService.authOut).toHaveBeenCalledWith(params);
    });
  });

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

The test associated to the usersTicketing method is the following one:

  describe('usersTicketing', () => {
    it('should return an object cotaining the users must be working list and moment.unix', async () => {
      Date.now = jest.fn(() => {
        const d = new Date(Date.UTC(2017, 10, 10, 8, 25));
        const nd = new Date(d.getTime() + d.getTimezoneOffset() * 60000);
        return nd.valueOf();
      });

      const result = await service.usersTicketing();

      expect(spyUserService.getUsersMustBeWorkingNow).toHaveBeenCalled();
      expect(result).toEqual({
        users: true,
        timestamp: 1510298700,
      });
    });
  });

In this case a spy is created to be used when the function now from Date is executed. In this case ever return the same day (the test must be pure and don't depends of external factors). Therefore, in this test we need check the method getUsersMustBeWorkingNow has been invoked and that the result of the method usersTicketing is an object which contains the key users with the value provided in the spy UserService and the timestamp of the day mocked.

User Service

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

import { Test } from '@nestjs/testing';
import { TestingModule } from '@nestjs/testing/testing-module';
import { UserService } from './users.service';
import { User } from '../../users/entities/user.entity';
import { USER_REPOSITORY_TOKEN } from '../../../common/config/database.tokens.constants';
import * as moment from 'moment';
import {
  HOUR_FORMAT,
  FIRST_HOUR_MORNING,
  LAST_HOUR_MORNING,
  SCHEDULE_EXCLUDE,
  FIRST_HOUR_NIGHT,
  LAST_HOUR_NIGHT,
} from '../constants/users.constans';

describe('User service', () => {
  let testingModule: TestingModule;
  let service: UserService;
  let spyRepository: any;

  beforeEach(async () => {
    testingModule = await Test.createTestingModule({
      providers: [
        UserService,
        {
          provide: USER_REPOSITORY_TOKEN,
          useFactory: () => ({
            save: jest.fn(() => true),
          }),
        },
      ],
    }).compile();

    service = testingModule.get(UserService);
    spyRepository = testingModule.get(USER_REPOSITORY_TOKEN);
  });
  
  ...
  

The first method is very easy because the technique used is the same that in the app.service.ts. Hence, the code to test is the following one:

  public addUser(userDto: { uid: string; key: string }): Promise<User> {
    return this.usersRepository.save(Object.assign(new User(), userDto));
  }

And its test suite only checks if the method save is invoked with the right parameters (User prototype and initial params) as you can see in the following code:

  describe('addUser', () => {
    it('should save an user in the database', async () => {
      const params = {
        uid: 'uid',
        key: 'key',
      };

      spyRepository.save = jest.fn();
      await service.addUser(params);

      expect(spyRepository.save).toHaveBeenCalledWith(
        Object.assign(new User(), params),
      );
    });
  });

The next method to test is a call to the TypeORM ORM which you can see below:

  public getUsersWithoutKey(): Promise<UserEntity[]> {
    return this.usersRepository
      .createQueryBuilder('user')
      .select('user.uid')
      .where('user.key IS NULL')
      .getMany();
  }

In this test, we need spy each method from the usersRepository using chain responsability. So, the method to do this we use the factory that Jest provides.

  describe('getUsersWithoutKey', () => {
    it('should return all users who have not key', async () => {
      const getMany = jest.fn();
      const where = jest.fn(() => ({ getMany }));
      const select = jest.fn(() => ({ where }));
      spyRepository.createQueryBuilder = jest.fn(() => ({ select }));

      await service.getUsersWithoutKey();
      expect(spyRepository.createQueryBuilder).toHaveBeenCalledWith('user');
      expect(select).toHaveBeenCalledWith('user.uid');
      expect(where).toHaveBeenCalledWith('user.key IS NULL');
    });
  });

As you see we are checking every single method from TypeORM which was called and which parameters was called with, easy and quick.

The following method could has a famous code smell (long method) but if you read carefully the method, you will notice that it is a great invocation to a database query and the code has not a code smell.

public getUsersMustBeWorkingNow() {
    const date = moment();
    const dayOfWeek = date.day() - 1;
    const hourNow = this.convertBetweenRealHourAndScheduleHour(date);
    const isMorning = this.isMorning(date);

    const users = this.usersRepository
      .createQueryBuilder('user')
      .innerJoinAndSelect('user.schedule', 'schedule')
      .leftJoinAndSelect(
        'user.auths',
        'auths',
        '(auths.timestamp > :lowerHour AND auths.timestamp < :upperHour)',
        {
          lowerHour: isMorning
            ? moment(FIRST_HOUR_MORNING, HOUR_FORMAT).unix()
            : moment(FIRST_HOUR_NIGHT, HOUR_FORMAT).unix(),
          upperHour: isMorning
            ? moment(LAST_HOUR_MORNING, HOUR_FORMAT).unix()
            : moment(LAST_HOUR_NIGHT, HOUR_FORMAT).unix(),
        },
      )
      .where('schedule.day = :dayOfWeek', {
        dayOfWeek,
      })
      .andWhere('schedule.hour = :hourNow', {
        hourNow,
      })
      .andWhere('schedule.room NOT IN (:...exclude)', {
        exclude: SCHEDULE_EXCLUDE,
      })
      .getMany();
    return users;
  }

The query has several combination of parameters but the test will be the same, therefore to do this test we need a table of inputs and outputs in our test. Jest has a parameter called each which can be used to parameterize our test.

The table is the following:

describe('getUsersMustBeWorkingNow', () => {
    it.each`
      year    | month | day   | hour  | minute | seconds | hourNowExpected | dayNowExpected
      ${2017} | ${1}  | ${2}  | ${10} | ${0}   | ${0}    | ${1}            | ${3}
      ${2017} | ${1}  | ${3}  | ${8}  | ${15}  | ${0}    | ${0}            | ${4}
      ${2017} | ${1}  | ${6}  | ${8}  | ${15}  | ${1}    | ${0}            | ${0}
      ${2017} | ${1}  | ${7}  | ${11} | ${15}  | ${1}    | ${-1}           | ${1}
      ${2017} | ${1}  | ${8}  | ${11} | ${45}  | ${1}    | ${3}            | ${2}
      ${2017} | ${1}  | ${9}  | ${13} | ${44}  | ${59}   | ${4}            | ${3}
      ${2017} | ${1}  | ${10} | ${14} | ${44}  | ${59}   | ${5}            | ${4}
      ${2017} | ${1}  | ${13} | ${15} | ${44}  | ${1}    | ${-1}           | ${0}
      ${2017} | ${1}  | ${14} | ${16} | ${0}   | ${1}    | ${6}            | ${1}
      ${2017} | ${1}  | ${15} | ${17} | ${0}   | ${1}    | ${7}            | ${2}
      ${2017} | ${1}  | ${16} | ${19} | ${59}  | ${59}   | ${9}            | ${3}
      ${2017} | ${1}  | ${17} | ${20} | ${9}   | ${59}   | ${-1}           | ${4}
      ${2017} | ${1}  | ${20} | ${20} | ${10}  | ${0}    | ${10}           | ${0}
      ${2017} | ${1}  | ${21} | ${22} | ${9}   | ${59}   | ${11}           | ${1}
      ${2017} | ${1}  | ${22} | ${22} | ${10}  | ${1}    | ${-1}           | ${2}
    `(
      'should return all users who must be working now',
      async ({
        year,
        month,
        day,
        hour,
        minute,
        seconds,
        hourNowExpected,
        dayNowExpected,
      }) => {
      ...

You can see that the parameters used to testing in our table are the following:

  • year: Year corresponding to the moment in which we want to test if the user are in the building.
  • month: Month corresponding to the moment in which we want to test if the user are in the building.
  • day: Day corresponding to the moment in which we want to test if the user are in the building.
  • hour: Hour corresponding to the moment in which we want to test if the user are in the building.
  • minute: Minute corresponding to the moment in which we want to test if the user are in the building.
  • seconds: Seconds corresponding to the moment in which we want to test if the user are in the building.
  • hourNowExpected: Hour which should return the method using the other list of parameters.
  • dayNowExpected: Day which should return the method using the other list of parameters.

Our test need a lot of spies to testing the ORM and the expected values from the table are used to check that the privates methods return the values that will be used to the ORM query.The test will be easier if the private methods were public but a test should never change the original code (only when a bug is discovered).

}) => {
        const getMany = jest.fn();
        const andWhere2 = jest.fn(() => ({ getMany }));
        const andWhere1 = jest.fn(() => ({ andWhere: andWhere2 }));
        const where1 = jest.fn(() => ({ andWhere: andWhere1 }));
        const leftJoinAndSelect = jest.fn(() => ({ where: where1 }));
        const innerJoinAndSelect1 = jest.fn(() => ({ leftJoinAndSelect }));
        spyRepository.createQueryBuilder = jest.fn(() => ({
          innerJoinAndSelect: innerJoinAndSelect1,
        }));

        Date.now = jest.fn(() => {
          const d = new Date(Date.UTC(year, month, day, hour, minute, seconds));
          const nd = new Date(d.getTime() + d.getTimezoneOffset() * 60000);
          return nd.valueOf();
        });
        const isMorning = hour < 15;

        await service.getUsersMustBeWorkingNow();

        expect(spyRepository.createQueryBuilder).toHaveBeenCalledWith('user');
        expect(innerJoinAndSelect1).toHaveBeenCalledWith(
          'user.schedule',
          'schedule',
        );
        expect(leftJoinAndSelect).toHaveBeenCalledWith(
          'user.auths',
          'auths',
          '(auths.timestamp > :lowerHour AND auths.timestamp < :upperHour)',
          {
            lowerHour: isMorning
              ? moment(FIRST_HOUR_MORNING, HOUR_FORMAT).unix()
              : moment(FIRST_HOUR_NIGHT, HOUR_FORMAT).unix(),
            upperHour: isMorning
              ? moment(LAST_HOUR_MORNING, HOUR_FORMAT).unix()
              : moment(LAST_HOUR_NIGHT, HOUR_FORMAT).unix(),
          },
        );
        expect(where1).toHaveBeenCalledWith('schedule.day = :dayOfWeek', {
          dayOfWeek: dayNowExpected,
        });
        expect(andWhere1).toHaveBeenCalledWith('schedule.hour = :hourNow', {
          hourNow: hourNowExpected,
        });
        expect(andWhere2).toHaveBeenCalledWith(
          'schedule.room NOT IN (:...exclude)',
          {
            exclude: SCHEDULE_EXCLUDE,
          },
        );
      },
    );

The first part of the test is the creation of spies to check that it is being called using the correct parameters. Then, the method service.getUsersMustBeWorkingNow() gets invoked. Finally, there are a list of expects which check that the ORM's method are invoked using the correct parameters.

So, the final code of this test is the following:

  describe('getUsersMustBeWorkingNow', () => {
    it.each`
      year    | month | day   | hour  | minute | seconds | hourNowExpected | dayNowExpected
      ${2017} | ${1}  | ${2}  | ${10} | ${0}   | ${0}    | ${1}            | ${3}
      ${2017} | ${1}  | ${3}  | ${8}  | ${15}  | ${0}    | ${0}            | ${4}
      ${2017} | ${1}  | ${6}  | ${8}  | ${15}  | ${1}    | ${0}            | ${0}
      ${2017} | ${1}  | ${7}  | ${11} | ${15}  | ${1}    | ${-1}           | ${1}
      ${2017} | ${1}  | ${8}  | ${11} | ${45}  | ${1}    | ${3}            | ${2}
      ${2017} | ${1}  | ${9}  | ${13} | ${44}  | ${59}   | ${4}            | ${3}
      ${2017} | ${1}  | ${10} | ${14} | ${44}  | ${59}   | ${5}            | ${4}
      ${2017} | ${1}  | ${13} | ${15} | ${44}  | ${1}    | ${-1}           | ${0}
      ${2017} | ${1}  | ${14} | ${16} | ${0}   | ${1}    | ${6}            | ${1}
      ${2017} | ${1}  | ${15} | ${17} | ${0}   | ${1}    | ${7}            | ${2}
      ${2017} | ${1}  | ${16} | ${19} | ${59}  | ${59}   | ${9}            | ${3}
      ${2017} | ${1}  | ${17} | ${20} | ${9}   | ${59}   | ${-1}           | ${4}
      ${2017} | ${1}  | ${20} | ${20} | ${10}  | ${0}    | ${10}           | ${0}
      ${2017} | ${1}  | ${21} | ${22} | ${9}   | ${59}   | ${11}           | ${1}
      ${2017} | ${1}  | ${22} | ${22} | ${10}  | ${1}    | ${-1}           | ${2}
    `(
      'should return all users who must be working now',
      async ({
        year,
        month,
        day,
        hour,
        minute,
        seconds,
        hourNowExpected,
        dayNowExpected,
      }) => {
        const getMany = jest.fn();
        const andWhere2 = jest.fn(() => ({ getMany }));
        const andWhere1 = jest.fn(() => ({ andWhere: andWhere2 }));
        const where1 = jest.fn(() => ({ andWhere: andWhere1 }));
        const leftJoinAndSelect = jest.fn(() => ({ where: where1 }));
        const innerJoinAndSelect1 = jest.fn(() => ({ leftJoinAndSelect }));
        spyRepository.createQueryBuilder = jest.fn(() => ({
          innerJoinAndSelect: innerJoinAndSelect1,
        }));

        Date.now = jest.fn(() => {
          const d = new Date(Date.UTC(year, month, day, hour, minute, seconds));
          const nd = new Date(d.getTime() + d.getTimezoneOffset() * 60000);
          return nd.valueOf();
        });
        const isMorning = hour < 15;

        await service.getUsersMustBeWorkingNow();

        expect(spyRepository.createQueryBuilder).toHaveBeenCalledWith('user');
        expect(innerJoinAndSelect1).toHaveBeenCalledWith(
          'user.schedule',
          'schedule',
        );
        expect(leftJoinAndSelect).toHaveBeenCalledWith(
          'user.auths',
          'auths',
          '(auths.timestamp > :lowerHour AND auths.timestamp < :upperHour)',
          {
            lowerHour: isMorning
              ? moment(FIRST_HOUR_MORNING, HOUR_FORMAT).unix()
              : moment(FIRST_HOUR_NIGHT, HOUR_FORMAT).unix(),
            upperHour: isMorning
              ? moment(LAST_HOUR_MORNING, HOUR_FORMAT).unix()
              : moment(LAST_HOUR_NIGHT, HOUR_FORMAT).unix(),
          },
        );
        expect(where1).toHaveBeenCalledWith('schedule.day = :dayOfWeek', {
          dayOfWeek: dayNowExpected,
        });
        expect(andWhere1).toHaveBeenCalledWith('schedule.hour = :hourNow', {
          hourNow: hourNowExpected,
        });
        expect(andWhere2).toHaveBeenCalledWith(
          'schedule.room NOT IN (:...exclude)',
          {
            exclude: SCHEDULE_EXCLUDE,
          },
        );
      },
    );
  });

Auth Service

The last service to testing is auth.service.ts. The technique to use is the similar to the previous test. So, the first step is the configuration initial in each test.

import { Test } from '@nestjs/testing';
import { TestingModule } from '@nestjs/testing/testing-module';
import { AuthService } from './auth.service';
import {
  AUTH_REPOSITORY_TOKEN,
  USER_REPOSITORY_TOKEN,
} from '../../common/config/database.tokens.constants';
import { AuthDto } from './dto/auth.dto';
import { User } from '../users/entities/user.entity';
import {
  STATUS_CODE_RESPONSE,
  INPUT,
  OUTPUT,
} from './constants/auth.constants';

describe('Auth service', () => {
  let testingModule: TestingModule;
  let service: AuthService;
  let spyAuthRepository: any;
  let spyUserRepository: any;

  beforeEach(async () => {
    testingModule = await Test.createTestingModule({
      providers: [
        AuthService,
        {
          provide: AUTH_REPOSITORY_TOKEN,
          useFactory: () => ({
            findOne: jest.fn(() => true),
            save: jest.fn(() => true),
          }),
        },
        {
          provide: USER_REPOSITORY_TOKEN,
          useFactory: () => ({
            save: jest.fn(() => true),
          }),
        },
      ],
    }).compile();

    service = testingModule.get(AuthService);
    spyAuthRepository = testingModule.get(AUTH_REPOSITORY_TOKEN);
    spyUserRepository = testingModule.get(USER_REPOSITORY_TOKEN);
  });
  
  ...

The code to test is the following one:

async authIn(auth: AuthDto): Promise<AuthResponseDto> {
    try {
      const user = await this.saveTicketing({ ...auth, reader: INPUT });
      return this.welcomeTeacher(user.name);
    } catch (e) {
      return { status: STATUS_CODE_RESPONSE.KO, msg: 'Error en la entrada' };
    }
  }
  async authOut(auth: AuthDto): Promise<AuthResponseDto> {
    try {
      const user = await this.saveTicketing({ ...auth, reader: OUTPUT });
      return this.byeTeacher(user.name);
    } catch (e) {
      return { status: STATUS_CODE_RESPONSE.KO, msg: 'Error en la salida' };
    }
  }

You can see that there are several private methods which are not possible to test directly due to the private methods are similar to copy/paste this code in the public method. Therefore, these methods have not a test suite.

The private methods are the following:

  private async saveTicketing(auth: AuthDto): Promise<User> {
    const user = await this.userRepository.findOne({
      where: {
        key: auth.key,
      },
    });
    if (!user) {
      throw new Error();
    }
    await this.authRepository.save({
      ...auth,
      user,
      timestamp: moment().unix(),
    });
    return user;
  }

  private welcomeTeacher(nameTeacher) {
    return {
      status: STATUS_CODE_RESPONSE.OK,
      msg: `Entrada - ${nameTeacher}`,
    };
  }
  private byeTeacher(nameTeacher) {
    return {
      status: STATUS_CODE_RESPONSE.OK,
      msg: `Salida - ${nameTeacher}`,
    };
  }

In our test suite of the methods authIn and authOut there are three different test which represent a scenario as you can see below.

  1. authIn.
    2. should have authentication and return greetings.
    3. should return error when the user is not found.
    4. should return error when unexpected error appears.
  2. authOut.
    6. should save authentication and return bye.
    7. should return error when the user is not found.
    8. should return error when unexpected error appears.

The authIn code is the following:

  describe('authIn', () => {
    it('should save authentication and return greetings', async () => {
      const params: AuthDto = {
        key: 'key',
        reader: 'reader',
      };
      const user: Partial<User> = {
        uid: '1',
        name: 'nameUser',
      };

      spyUserRepository.findOne = jest.fn(() => user);
      spyAuthRepository.save = jest.fn();
      const greeting = await service.authIn(params);
      expect(greeting).toEqual({
        status: STATUS_CODE_RESPONSE.OK,
        msg: `Entrada - ${user.name}`,
      });
      expect(spyUserRepository.findOne).toHaveBeenCalledWith({
        where: { key: params.key },
      });
      expect(spyAuthRepository.save).toHaveBeenCalledWith({
        ...params,
        reader: INPUT,
        user,
        timestamp: expect.any(Number),
      });
    });
    it('should return error when the user is not found', async () => {
      const params = {} as AuthDto;
      spyUserRepository.findOne = jest.fn(() => undefined);

      const error = await service.authIn(params);

      expect(spyAuthRepository.save).not.toHaveBeenCalled();
      expect(error).toEqual({
        status: STATUS_CODE_RESPONSE.KO,
        msg: 'Error en la entrada',
      });
    });
    it('should return error when unexpected error appears', async () => {
      const params = {} as AuthDto;
      spyUserRepository.findOne = jest.fn(() => ({} as User));
      spyAuthRepository.save = jest.fn(() => {
        throw new Error();
      });
      const error = await service.authIn(params);

      expect(error).toEqual({
        status: STATUS_CODE_RESPONSE.KO,
        msg: 'Error en la entrada',
      });
    });
  });

And the authOut code is the following one:

  describe('authOut', () => {
    it('should save authentication and return bye', async () => {
      const params: AuthDto = {
        key: 'key',
        reader: 'reader',
      };
      const user: Partial<User> = {
        uid: '1',
        name: 'nameUser',
      };

      spyUserRepository.findOne = jest.fn(() => user);
      spyAuthRepository.save = jest.fn();
      const greeting = await service.authOut(params);
      expect(greeting).toEqual({
        status: STATUS_CODE_RESPONSE.OK,
        msg: `Salida - ${user.name}`,
      });
      expect(spyUserRepository.findOne).toHaveBeenCalledWith({
        where: { key: params.key },
      });
      expect(spyAuthRepository.save).toHaveBeenCalledWith({
        ...params,
        reader: OUTPUT,
        user,
        timestamp: expect.any(Number),
      });
    });
    it('should return error when the user is not found', async () => {
      const params = {} as AuthDto;
      spyUserRepository.findOne = jest.fn(() => undefined);

      const error = await service.authOut(params);

      expect(spyAuthRepository.save).not.toHaveBeenCalled();
      expect(error).toEqual({
        status: STATUS_CODE_RESPONSE.KO,
        msg: 'Error en la salida',
      });
    });
    it('should return error when unexpected error appears', async () => {
      const params = {} as AuthDto;
      spyUserRepository.findOne = jest.fn(() => ({} as User));
      spyAuthRepository.save = jest.fn(() => {
        throw new Error();
      });
      const error = await service.authOut(params);

      expect(error).toEqual({
        status: STATUS_CODE_RESPONSE.KO,
        msg: 'Error en la salida',
      });
    });
  });

Conclusion

In this post I have explained how you can test services of your backend using jest and the NestJS Framework. The most interesting feature of this code, is the fact that we can use spies to isolate our tests and we can create a table of inputs and outputs to automate a lot of test that are the same but using different parameters.

In the next post, I will show you how you can unit test of the controllers.