Integration testing Node.js apps

January 25, 2023

Integration testing means testing a component with multiple sub-components and how they interact. Some sub-components can be external services, databases, and message queues.

External services are running, but their business logic is mocked based on received parameters (request headers, query parameters, etc.). Databases and message queues are spun up using test containers.

This post covers testing service as a component and its API endpoints. This approach can be used with any framework and language. NestJS and Express are used in the examples below.

API endpoints

Below is the controller for two endpoints. First communicates with an external service and retrieves some data based on the sent parameter. The second one retrieves the data from the database.

// users.controller.ts
export class UsersController {
constructor(private userService: UsersService) {}
async getAll(@Query('type') type: string) {
return this.userService.findAll(type);
async getById(@Param('id', new ParseUUIDPipe()) id: string) {
return this.userService.findById(id);

External dependencies

External service is mocked to send data based on the received parameter.

export const createDummyUserServiceServer = async (): Promise<DummyServer> => {
return createDummyServer((app) => {
app.get('/users', (req, res) => {
if (req.query.type !== 'user') {
return res.status(403).send('User type is not valid');

Tests setup

Tests for endpoints can be split into two parts. The first is related to the external dependencies setup.

The example below creates a mocked service and spins up the database using test containers. The environment variables are set for before mentioned dependencies, and the leading service starts running.

The database is cleaned before every test run. External dependencies (mocked service and database) are closed after tests finish.

// test/users.spec.ts
describe('UsersController (integration)', () => {
let app: INestApplication;
let dummyUserServiceServerClose: () => void;
let postgresContainer: StartedTestContainer;
let usersRepository: Repository<UsersEntity>;
const databaseConfig = {
databaseName: 'nestjs-starter-db',
databaseUsername: 'user',
databasePassword: 'some-r4ndom-pasS',
databasePort: 5432,
beforeAll(async () => {
const dummyUserServiceServer = await createDummyUserServiceServer();
dummyUserServiceServerClose = dummyUserServiceServer.close;
postgresContainer = await new GenericContainer('postgres:15-alpine')
POSTGRES_USER: databaseConfig.databaseUsername,
POSTGRES_PASSWORD: databaseConfig.databasePassword,
POSTGRES_DB: databaseConfig.databaseName,
const moduleFixture: TestingModule = await Test.createTestingModule({
imports: [AppModule],
get: (key: string): string => {
const map: Record<string, string | undefined> = process.env;
map.USER_SERVICE_URL = dummyUserServiceServer.url;
map.DATABASE_HOSTNAME = postgresContainer.getHost();
map.DATABASE_PORT = `${postgresContainer.getMappedPort(databaseConfig.databasePort)}`;
map.DATABASE_NAME = databaseConfig.databaseName;
map.DATABASE_USERNAME = databaseConfig.databaseUsername;
map.DATABASE_PASSWORD = databaseConfig.databasePassword;
return map[key] || '';
app = moduleFixture.createNestApplication();
usersRepository = app.get(getRepositoryToken(UsersEntity));
await app.init();
beforeEach(async () => {
await usersRepository.delete({});
afterAll(async () => {
await app.close();
await postgresContainer.stop();
// ...


The second part covers tests for the implemented endpoints. The first test suite asserts retrieving data from the external service based on the sent type as a query parameter.

// test/users.spec.ts
describe('/users (GET)', () => {
it('should return list of users', async () => {
return request(app.getHttpServer())
.then((response) => {
it('should throw an error when type is forbidden', async () => {
return request(app.getHttpServer())

The second test suite asserts retrieving the data from the database.

// test/users.spec.ts
describe('/users/:id (GET)', () => {
it('should return found user', async () => {
const userId = 'b618445a-0089-43d5-b9ca-e6f2fc29a11d';
const userDetails = {
id: userId,
firstName: 'tester',
const newUser = await usersRepository.create(userDetails);
await usersRepository.save(newUser);
return request(app.getHttpServer())
.then((response) => {
it('should return 404 error when user is not found', async () => {
const userId = 'b618445a-0089-43d5-b9ca-e6f2fc29a11d';
return request(app.getHttpServer())


Here is the link to the boilerplate with the mentioned examples.


Testing custom repositories (NestJS/TypeORM)

September 5, 2021

Custom repositories extend the base repository class and enrich it with several additional methods. This post covers the unit and integration testing.

// user.repository.ts
export class UserRepository extends Repository<UserEntity> {
constructor(private dataSource: DataSource) {
super(UserEntity, dataSource.createEntityManager());
async getById(id: string) {
return this.findOne({ where: { id } });
// ...


Inject a custom repository into the service.

// user.service.ts
export class UserService {
constructor(private readonly userRepository: UserRepository) {}
async getById(id: string): Promise<User> {
return this.userRepository.getById(id);
// ...

Pass entity class to the forFeature method.

// user.module.ts
imports: [
// ...
providers: [UserService, UserRepository],
// ...
export class UserModule {}

Unit testing

To properly unit-test the custom repository, mock some methods.

// user.repository.spec.ts
describe('UserRepository', () => {
let userRepository: UserRepository;
const dataSource = {
createEntityManager: jest.fn()
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
provide: DataSource,
useValue: dataSource
userRepository = module.get<UserRepository>(UserRepository);
describe('getById', () => {
it('should return found user', async () => {
const id = 'id';
const user = {
const findOneSpy = jest
.spyOn(userRepository, 'findOne')
.mockResolvedValue(user as UserEntity);
const foundUser = await userRepository.getById(id);
expect(findOneSpy).toHaveBeenCalledWith({ where: user });

Integration testing

Integration testing is more suitable when working with databases. Read more about it on Integration testing Node.js apps post

Spies and mocking with Jest

August 19, 2021

Besides asserting the output of the function call, unit testing includes the usage of spies and mocking. Spies are functions that let you spy on the behavior of functions called indirectly by some other code. Spy can be created by using jest.fn(). Mocking injects test values into the code during the tests. Some of the use cases will be presented below.

  • Async function and its resolved value can be mocked using mockResolvedValue. Another way to mock it is by using mockImplementation and providing a function as an argument.
const calculationService = {
calculate: jest.fn()
jest.spyOn(calculationService, 'calculate').mockResolvedValue(value);
.spyOn(calculationService, 'calculate')
.mockImplementation(async (a) => Promise.resolve(a));
  • Rejected async function can be mocked using mockRejectedValue and mockImplementation.
.spyOn(calculationService, 'calculate')
.mockRejectedValue(new Error(errorMessage));
.spyOn(calculationService, 'calculate')
.mockImplementation(async () => Promise.reject(new Error(errorMessage)));
await expect(calculateSomething(calculationService)).rejects.toThrowError(
  • Sync function and its return value can be mocked using mockReturnValue and mockImplementation.
jest.spyOn(calculationService, 'calculate').mockReturnValue(value);
jest.spyOn(calculationService, 'calculate').mockImplementation((a) => a);
  • Chained methods can be mocked using mockReturnThis.
// calculationService.get().calculate();
jest.spyOn(calculationService, 'get').mockReturnThis();
  • Async and sync functions called multiple times can be mocked with different values using mockResolvedValueOnce and mockReturnValueOnce, respectively, and mockImplementationOnce.
.spyOn(calculationService, 'calculate')
.spyOn(calculationService, 'calculate')
.spyOn(calculationService, 'calculate')
.mockImplementationOnce((a) => a + 3)
.mockImplementationOnce((a) => a + 5);
  • External modules can be mocked similarly to spies. For the following example, let's suppose axios package is already used in one function. The following example represents a test file where axios is mocked using jest.mock().
import axios from 'axios';
// within test case
  • Manual mocks are resolved by writing corresponding modules in __mocks__ directory, e.g., fs/promises mock will be stored in __mocks__/fs/promises.js file. fs/promises mock will be resolved using jest.mock() in the test file.
  • To assert called arguments for a mocked function, an assertion can be done using toHaveBeenCalledWith matcher.
const spy = jest.spyOn(calculationService, 'calculate');
expect(spy).toHaveBeenCalledWith(firstArgument, secondArgument);
  • To assert skipped call for a mocked function, an assertion can be done using not.toHaveBeenCalled matcher.
const spy = jest.spyOn(calculationService, 'calculate');
  • To assert called arguments for the exact call when a mocked function is called multiple times, an assertion can be done using toHaveBeenNthCalledWith matcher.
const argumentsList = [0, 1];
argumentsList.forEach((argumentList, index) => {
index + 1,
  • Methods should be restored to their initial implementation before each test case.
// package.json
"jest": {
// ...
"restoreMocks": true
// ...

Further reading


© 2023