DEV Community

tkssharma
tkssharma

Posted on

Unit Testing and Integration Testing Nestjs Application

We will use Jest as Runner for Running tests for NestJS

Lets first talk about Jest a little bit which is a popular test runner

Jest is a JavaScript test runner, that is, a JavaScript library for creating, running, and structuring tests.

Jest ships as an NPM package, you can install it in any JavaScript project. Jest is one of the most popular test runner these days, and the default choice for React projects.

Setting up the project

As with every JavaScript project you'll need an NPM environment (make sure to have Node installed on your system). Create a new folder and initialize the project with:



mkdir getting-started-with-jest && cd $_
npm init -y


Enter fullscreen mode Exit fullscreen mode

Next up install Jest with:



npm i jest --save-dev
Let's also configure an NPM script for running our tests from the command line. Open up package.json and configure a script named test for running Jest:

  "scripts": {
    "test": "jest"
  },


Enter fullscreen mode Exit fullscreen mode

Jest can be used with any javascript application React, angular, nestjs, Vue JS, express..

and you're good to go!

Before we begin, let's refresh the basic concepts of unit testing.

Unit testing focuses on writing tests for the smallest possible units. In most cases, they are functions defined in classes. MethodA in a class may be calling MethodB in another class. However, a unit test of MethodA is focused only on the logic of MethodA, not MethodB. Unit tests shouldn’t be dependent on the environment in which they are being run, and they are supposed to be fast. To write isolated unit tests, it’s common to mock all dependencies of a method/service. In the StudentService unit test, we’ll mock AppService by creating an ApiServiceMock class.
Test Doubles: Fakes, stubs, and mocks all belong to the category of test doubles. A test double is an object or system you use in a test instead of something else.

Fakes: an object with limited capabilities (for the purposes of testing), e.g. a fake web service. Fake has business behavior. You can drive a fake to behave in different ways by giving it different data. Fakes can be used when you can’t use a real implementation in your test.

Mock: an object on which you set expectations. A mock has expectations about the way it should be called, and a test should fail if it’s not called that way. Mocks are used to test interactions between objects.

Stub: an object that provides predefined answers to method calls. A stub has no logic and only returns what you tell it to return.
In case you are interested, here is a good discussion on fake/mock/stub.

Spy: Spy, spies on the caller. Often used to make sure a particular method has been called.

The job of a unit test is to verify an individual piece of code. A tested unit can be a module, a class, or a function. Each of our tests should be isolated and independent of each other. By writing unit tests, we can make sure that individual parts of our application work as expected.

Let’s write some tests for the ApiService.
How a basic unit test looks like in nestjs




import { Test, TestingModule } from '@nestjs/testing';
import { ApiService } from './api.service';
import { HttpModule } from '@nestjs/common';

describe('ApiService', () => {
  let service: ApiService;

  beforeEach(async () => {
    const module: TestingModule = await Test.createTestingModule({
      providers: [ApiService],
      imports: [HttpModule],
    }).compile();

    service = module.get<ApiService>(ApiService);
  });

  it('ApiService - should be defined', () => {
    expect(service).toBeDefined();
  });
});


Enter fullscreen mode Exit fullscreen mode

Lets break this down first we are getting TestingModule by calling Test.createTestingModule, once we have module we can get services and controllers from module and test their methods with mock already done while creating test module



beforeEach(async () => {
    const module: TestingModule = await Test.createTestingModule({
      providers: [ApiService],
      imports: [HttpModule],
    }).compile();
    // getting service modue from main module
    service = module.get<ApiService>(ApiService);
  });
  // now checking if service is available 
  it('ApiService - should be defined', () => {
    expect(service).toBeDefined();
  });


Enter fullscreen mode Exit fullscreen mode

Lets say we are writing simple CRUD application and it has controllers and services
Here we will talk about different ways of testing controllers and services using Jest
My controllers looks like this which is talking to service to fetch data from typeORM repositories
Link : https://github.com/tkssharma/12-factor-app-microservices/tree/master/nestjs-cli-baseline

Testing Controllers



import {
    Get,
    Post,
    Controller,
    HttpCode,
    HttpStatus,
    Body,
    Req,
    Param,
    ParamData,
    Patch,
    Delete,
  ValidationPipe,
  UsePipes,
} from '@nestjs/common';
import NoteService  from '../services/note.service';
import { CreateNoteDto, GetNoteById } from '../dto/create-note.dto';
import { UpdateNoteDto } from '../dto/update-note-dto';
import { ApiOperation, ApiTags } from '@nestjs/swagger';

@Controller('/api/v1/notes')
export class NoteController {
    constructor(private readonly noteService: NoteService) {}

    @Post()
  @ApiTags('notes')
  @ApiOperation({ description: 'Get All categories or sub-categories' })
  @UsePipes(ValidationPipe)
  @HttpCode(HttpStatus.CREATED)
    async saveNote(@Body() dto: CreateNoteDto) {
        return await this.noteService.saveNote(dto);
    }

  @ApiTags('notes')
  @Get('/')
  @ApiOperation({ description: 'Get All categories or sub-categories' })
  @UsePipes(ValidationPipe)
  @HttpCode(HttpStatus.OK)
    async getAllNote() {
        return await this.noteService.findAllNotes({});
    }

  @ApiTags('notes')
  @ApiOperation({ description: 'Get All categories or sub-categories' })
  @UsePipes(ValidationPipe)
  @HttpCode(HttpStatus.OK)
    @Get('/:id')
    async getNoteById(@Param() dto: GetNoteById) {
        return await this.noteService.findOneNote({
            where: {
                id: dto.id,
            },
        });
    }

  @ApiTags('notes')
  @ApiOperation({ description: 'Get All categories or sub-categories' })
  @UsePipes(ValidationPipe)
  @HttpCode(HttpStatus.OK)
    @Patch('/:id')
    async updateNoteById(
        @Param() param: GetNoteById,
        @Body() dto: UpdateNoteDto,
    ) {
        return await this.noteService.updateNote(param.id, dto);
    }

  @ApiTags('notes')
  @ApiOperation({ description: 'delete notes' })
  @UsePipes(ValidationPipe)
  @HttpCode(HttpStatus.OK)
    @Delete('/:id')
    async deleteNoteById(@Param() param: GetNoteById,) {
        return await this.noteService.deleteNote(param.id);
    }
}



Enter fullscreen mode Exit fullscreen mode

This controller is using notesService to fetch data from typeORM Repositories
so we have controllers -> services -> Repositories -> Database
So clearly here we have to mock service so we can test controllers independently
Test will simply looks like



import { Test, TestingModule } from '@nestjs/testing';
import { NoteController } from './note.controller';
import NoteService from '../services/note.service';
import { CreateNoteDto, GetNoteById } from '../dto/create-note.dto';

describe("NoteController Unit Tests", () => {
  let noteController: NoteController;
  let spyService: NoteService
  beforeAll(async () => {
    const ApiServiceProvider = {
      provide: NoteService,
      useFactory: () => ({
        saveNote: jest.fn(() => []),
        findAllNotes: jest.fn(() => []),
        findOneNote: jest.fn(() => { }),
        updateNote: jest.fn(() => { }),
        deleteNote: jest.fn(() => { })
      })
    }
    const app: TestingModule = await Test.createTestingModule({
      controllers: [NoteController],
      providers: [NoteService, ApiServiceProvider],
    }).compile();

    noteController = app.get<NoteController>(NoteController);
    spyService = app.get<NoteService>(NoteService);
  })

  it("calling saveNotes method", () => {
    const dto = new CreateNoteDto();
    expect(noteController.saveNote(dto)).not.toEqual(null);
  })

  it("calling saveNotes method", () => {
    const dto = new CreateNoteDto();
    noteController.saveNote(dto);
    expect(spyService.saveNote).toHaveBeenCalled();
    expect(spyService.saveNote).toHaveBeenCalledWith(dto);
  })

  it("calling getAllNote method", () => {
    noteController.getAllNote();
    expect(spyService.findAllNotes).toHaveBeenCalled();
  })

  it("calling find NoteById method", () => {
    const dto = new GetNoteById();
    dto.id = '3789';
    noteController.getNoteById(dto);
    expect(spyService.findOneNote).toHaveBeenCalled();
  })

});


Enter fullscreen mode Exit fullscreen mode

Important part in this testing is mocking service methods either we can create mock using jest.mock methods
os simply create object



  const ApiServiceProvider = {
      provide: NoteService,
      useFactory: () => ({
        saveNote: jest.fn(() => []),
        findAllNotes: jest.fn(() => []),
        findOneNote: jest.fn(() => { }),
        updateNote: jest.fn(() => { }),
        deleteNote: jest.fn(() => { })
      })
    }
    const app: TestingModule = await Test.createTestingModule({
      controllers: [NoteController],
      providers: [NoteService, ApiServiceProvider],
    }).compile();


Enter fullscreen mode Exit fullscreen mode

ApiServiceProvider is a mock version of NoteService service and we are overriding the definition of service with ApiServiceProvider where we are passing provide: serviceName and factory to override implementation

Testing services

We can also use useClass and Provide mock class , lets see that in testing for services
Here is our service class simple CRUD



import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository, FindOneOptions, FindManyOptions } from 'typeorm';
import Note from '../entity/note.entity';
import * as _ from 'lodash';
import { CreateNoteDto } from '../dto/create-note.dto';
import { UpdateNoteDto } from '../dto/update-note-dto';

@Injectable()
export default class NoteService {
    constructor(
        @InjectRepository(Note) private readonly noteRepository: Repository<Note>,
    ) {}
    async saveNote(dto: CreateNoteDto) {
        const note = new Note();
        note.text = dto.text;
    note.is_completed = dto.is_completed;
        return await this.noteRepository.save(note);
    }

    async findAllNotes(findAllOptions: FindManyOptions<Note>) {
        return await this.noteRepository.find(findAllOptions);
    }

    async findOneNote(findOneOptions: FindOneOptions<Note>) {
        return await this.noteRepository.findOne(findOneOptions);
    }

    async updateNote(noteId: string, dto: UpdateNoteDto) {
        const foundNote = await this.findOneNote({
            where: { id: noteId },
        });
        return await this.noteRepository.save(_.merge(foundNote, dto));
    }

    async deleteNote(noteId: string) {
        const foundNote = await this.findOneNote({
            where: { id: noteId },
        });
   if(foundNote) {
        await this.noteRepository.delete(foundNote);
        return foundNote;
   }
   return null;
    }
}


Enter fullscreen mode Exit fullscreen mode

To test this we can use same way as we did in controller, Mock service methods



import { Test, TestingModule } from '@nestjs/testing';
import NoteService from './note.service';
import { Repository, FindOneOptions, FindManyOptions } from 'typeorm';
import { CreateNoteDto } from '../dto/create-note.dto';
import { UpdateNoteDto } from '../dto/update-note-dto';

class ApiServiceMock {
  saveNote(dto: any) {
     return [];
  }
  findOneNote() {
    return [];
  }
  deleteNote(id: string) {
    return null;
  }
  updateNote(id: string, dto: any) {
    return [];
  }
}
describe.only("NoteService", () => {

  let noteService: NoteService;

  beforeAll(async () => {
    const ApiServiceProvider = {
      provide: NoteService,
      useClass: ApiServiceMock,
    }
    const module: TestingModule = await Test.createTestingModule({
      providers: [
        NoteService, ApiServiceProvider
      ],
    }).compile();
    noteService = module.get<NoteService>(NoteService);
  })

  it('should call saveNote method with expected params', async () => {
    const createNoteSpy = jest.spyOn(noteService, 'saveNote');
    const dto = new CreateNoteDto();
    noteService.saveNote(dto);
    expect(createNoteSpy).toHaveBeenCalledWith(dto);
  });

  it('should call findOneNote method with expected param', async () => {
    const findOneNoteSpy = jest.spyOn(noteService, 'findOneNote');
    const findOneOptions: FindOneOptions = {};
    noteService.findOneNote(findOneOptions);
    expect(findOneNoteSpy).toHaveBeenCalledWith(findOneOptions);
  });

  it('should call updateNote method with expected params', async () => {
    const updateNoteSpy = jest.spyOn(noteService, 'updateNote');
    const noteId = 'noteId';
    const dto = new UpdateNoteDto();
    noteService.updateNote(noteId, dto);
    expect(updateNoteSpy).toHaveBeenCalledWith(noteId, dto);
  });

  it('should call deleteNote method with expected param', async () => {
    const deleteNoteSpy = jest.spyOn(noteService, 'deleteNote');
    const noteId = 'noteId';
    noteService.deleteNote(noteId);
    expect(deleteNoteSpy).toHaveBeenCalledWith(noteId);
  });
})


Enter fullscreen mode Exit fullscreen mode

we create mockProvider class ApiServiceMock with all mock methods in it



 const ApiServiceProvider = {
      provide: NoteService,
      useClass: ApiServiceMock,
    }


Enter fullscreen mode Exit fullscreen mode

Mocking Database Connection and Repository

We can mock Repository directly there is a way of doing it



import { getRepositoryToken } from '@nestjs/typeorm';

providers: [
  {
    provide: getRepositoryToken(User),
    useValue: {},
  }
],


Enter fullscreen mode Exit fullscreen mode


import {Test, TestingModule} from '@nestjs/testing';
import NoteService  from './note.service';
import * as sinon from 'sinon';
import { getRepositoryToken } from '@nestjs/typeorm';
import Note from '../entity/note.entity';
import { Repository, FindOneOptions, FindManyOptions } from 'typeorm';
import { CreateNoteDto } from '../dto/create-note.dto';
import { UpdateNoteDto } from '../dto/update-note-dto';

describe("NoteService", () => {
   let noteService: NoteService;
   let sandbox : sinon.SinonSandbox;
  beforeAll(async() => {
    sandbox = sinon.createSandbox();
        const module: TestingModule = await Test.createTestingModule({
            providers: [
                NoteService,
                {
                    provide: getRepositoryToken(Note),
                    useValue: sinon.createStubInstance(Repository),
                },
            ],
        }).compile();
        noteService = module.get<NoteService>(NoteService);
  })

  it('should call saveNote method with expected params', async () => {
        const createNoteSpy = jest.spyOn(noteService, 'saveNote');
        const dto = new CreateNoteDto();
        noteService.saveNote(dto);
        expect(createNoteSpy).toHaveBeenCalledWith(dto);
    });

  it('should call findOneNote method with expected param', async () => {
        const findOneNoteSpy = jest.spyOn(noteService, 'findOneNote');
        const findOneOptions: FindOneOptions = {};
        noteService.findOneNote(findOneOptions);
        expect(findOneNoteSpy).toHaveBeenCalledWith(findOneOptions);
    });

    it('should call updateNote method with expected params', async () => {
        const updateNoteSpy = jest.spyOn(noteService, 'updateNote');
        const noteId = 'noteId';
        const dto = new UpdateNoteDto();
        noteService.updateNote(noteId, dto);
        expect(updateNoteSpy).toHaveBeenCalledWith(noteId, dto);
    });

    it('should call deleteNote method with expected param', async () => {
        const deleteNoteSpy = jest.spyOn(noteService, 'deleteNote');
        const noteId = 'noteId';
        noteService.deleteNote(noteId);
        expect(deleteNoteSpy).toHaveBeenCalledWith(noteId);
    });

    afterAll(async () => {
        sandbox.restore();
    });
})


Enter fullscreen mode Exit fullscreen mode

Changing the mock per test

We do not always want to mock something the same way in each test. To change our implementation between tests, we can use jest.Mock.



src/users/tests/users.service.spec.ts
import { Test } from '@nestjs/testing';
import { getRepositoryToken } from '@nestjs/typeorm';
import User from '../../users/user.entity';
import { UsersService } from '../../users/users.service';

describe('The UsersService', () => {
  let usersService: UsersService;
  let findOne: jest.Mock;
  beforeEach(async () => {
    findOne = jest.fn();
    const module = await Test.createTestingModule({
      providers: [
        UsersService,
        {
          provide: getRepositoryToken(User),
          useValue: {
            findOne
          }
        }
      ],
    })
      .compile();
    usersService = await module.get(UsersService);
  })
  describe('when getting a user by email', () => {
    describe('and the user is matched', () => {
      let user: User;
      beforeEach(() => {
        user = new User();
        findOne.mockReturnValue(Promise.resolve(user));
      })
      it('should return the user', async () => {
        const fetchedUser = await usersService.getByEmail('test@test.com');
        expect(fetchedUser).toEqual(user);
      })
    })
    describe('and the user is not matched', () => {
      beforeEach(() => {
        findOne.mockReturnValue(undefined);
      })
      it('should throw an error', async () => {
        await expect(usersService.getByEmail('test@test.com')).rejects.toThrow();
      })
    })
  })
});


Enter fullscreen mode Exit fullscreen mode

Testing external 3rd Party service

Finally to Mock any external 3rd Party service we can use overrideProvider method from nestjs
Here BlobUploadService is external service calling azure to upload a file to mock this we can override this will simple mock function
uploadToAzure which will return upload file link



describe('testing', () => {
  let app: INestApplication;
  let testUtils: TestUtils;
  let moduleRef: TestingModule;
  beforeEach(async () => {
    moduleRef = await Test.createTestingModule({
      imports: [AppModule],
      providers: [TestUtils, DatabaseService],
    })
      .overrideProvider(AuthService)
      .useValue({
        init: (token: string) => testUtils.getRandomUser(token),
      })
      .overrideProvider(BlobUploadService)
      .useValue({ uploadToAzure: () => 'http://test__link_logo.com' })
      .compile();

    app = moduleRef.createNestApplication();
    testUtils = moduleRef.get<TestUtils>(TestUtils);
    await testUtils.reloadFixtures();
    await app.init();
  });


Enter fullscreen mode Exit fullscreen mode

This is all about Unit Testing, i will add another Blog about E2E Testing with real database by hitting APIs and seeding data for all tables for testing
Here is the playlist if you want to see this in action
https://www.youtube.com/watch?v=kROllv22WHw&list=PLIGDNOJWiL18srI6BmFLfwDPvorTmyQ_c

Top comments (2)

Collapse
 
anuragp3773 profile image
anuragP3773

Hey man it doesn't work for me. idk how did you managed to do this without providing { provide: NoteService, useValue: mockService }
And without this there will be dependency error.

Collapse
 
vovella profile image
vovella

👍