Testing in Nest JS Integration-E2E Tests

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

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"
  },

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();
  });
});

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();
  });

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);
	}
}

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();
  })

});

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();

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;
	}
}

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);
  });
})

we create mockProvider class ApiServiceMock with all mock methods in it

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

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: {},
  }
],
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();
	});
})

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();
      })
    })
  })
});

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();
  });

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

Comments