Mocking our MongoDB while testing in NestJS

Sometimes when we are creating our tests files we start thinking of how are we going to test our database models, some people say that we just need to mock the driver (really difficult to mock all the driver I think), others just say that we need to use a real DB which is created just for testing... I like to do both but in this guide I will tell you how to do the second option without the need of a dedicated DB for testing.

This approach that I'm going to use works really well with the framework itself AND in a continuous integration flow.

Is important that you know a little about testing in NestJS (if not, you can learn more about it in their website here). NestJS uses Jest as default so we are going to use it and we will handle our connection with TypeORM since is the best fit in our structure.

Dependencies

NestJS almost includes everything we need to do our tests, we only need to install the library we are going to use to work with MongoDB (also the MongoDB driver of course) and one extra library to mock MongoDB.

First we start installing TypeORM (is possible to use Mongoose too but as I said, I think TypeORM is better for this structure) and the NestJS package for it.

npm i @nestjs/typeorm typeorm mongodb

Now we will install a package to mock MongoDB

npm i -D mongodb-memory-server

Actually, we are not currently mocking MongoDB... We are installing a MongoDB in our node_modules folder. If you want you can share the same DB with others projects (to save disk space), check here for more info here

Start our configuration

We will create a DB module only for testing (this way we follow the same structure of NestJS), in this module we are going to start our MongoDB and also we will have some objects that will help us to mock some processes.

Let's start with the DB module:

db-test.module.ts

import { TypeOrmModule } from '@nestjs/typeorm';
import { MongoMemoryServer } from 'mongodb-memory-server';

const mongod = new MongoMemoryServer();

export default (customOpts: any = {}) => TypeOrmModule.forRootAsync({
  useFactory: async () => {
    const port = await mongod.getPort();
    const database = await mongod.getDbName();

    return {
      type: 'mongodb',
      host: '127.0.0.1',
      port,
      database,
      entities: [__dirname + '/**/*.entity{.ts,.js}'],
      ...customOpts,
    };
  },
});

This default export do two things: It creates a dynamic module with the help of the module TypeOrmModule exported from @nestjs/typeorm, and at the same time it creates a new connection to the MongoDB that lives in our node_modules with the package mongodb-memory-server.

Something important to mention, I'm returning a function because we will need to send a parameter to the connection so we can do parallel testing, I will explain that later in this article.

Let's create a model and a service to test:

user.entity.ts

import { Entity, Column, ObjectIdColumn, BeforeInsert, ObjectID, BeforeUpdate } from 'typeorm';

export interface IUser {
  id?: ObjectID;
  firstName: string;
  lastName: string;
  email: string;
  createdAt: Date;
  updatedAt: Date;
}

@Entity()
export class UserEntity {
  @ObjectIdColumn()
  id: IUser['id'];

  @Column()
  firstName: IUser['firstName'];

  @Column()
  lastName: IUser['lastName'];

  @Column()
  email: IUser['email'];

  @Column()
  createdAt: IUser['createdAt'];

  @Column()
  updatedAt: IUser['updatedAt'];

  @BeforeInsert()
  setDefaultValue() {
    const newDate = new Date();
    this.createdAt = newDate;
    this.updatedAt = newDate;
  }

  @BeforeUpdate()
  updateDefaultValues() {
    this.updatedAt = new Date();
  }
}

user.service.ts

import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';

import { IUser, UserEntity } from './user.entity';

@Injectable()
export class UsersService {
  constructor(
    @InjectRepository(UserEntity) private readonly userRepository: Repository<UserEntity>,
  ) { }

  create(initialValue: Partial<IUser> = {}) {
    return this.userRepository.create(initialValue);
  }

  async save(userEntity: UserEntity) {
    return this.userRepository.save(userEntity);
  }
}

Here we have both a basic service and a basic entity for our Users . To keep things simple we are just creating and saving our users but we can do everything we want and test it.

Creating our test file

Here I'm going to paste all the file and then we will review each part of the file to understand what we are doing.

users.service.spec.ts

import { Test, TestingModule } from '@nestjs/testing';
import { TypeOrmModule } from '@nestjs/typeorm';
import { UsersService } from './users.service';
import { UserEntity } from './user.entity';
import DbModule from '../db-test.module';

const testUser = {
  email: 'test@test.com',
  firstName: 'earrieta',
  lastName: 'dev',
};

const wait = time => new Promise(resolve => setTimeout(() => resolve(time), time));

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

  beforeEach(async () => {
    const module: TestingModule = await Test.createTestingModule({
      imports: [
        DbModule({
          name: (new Date().getTime() * Math.random()).toString(16), // <-- This is to have a "unique" name for the connection
        }),
        TypeOrmModule.forFeature([
          UserEntity,
        ]),
      ],
      providers: [
        UsersService,
      ],
    }).compile();

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

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

  it('should return a new entity', () => {
    const user = service.create(testUser);

    expect(user).toEqual(testUser);
    expect(user instanceof UserEntity).toBe(true);
  });

  it('should save the user and add the createdAt and savedAt fields', async () => {
    const user = service.create(testUser);
    await service.save(user);

    expect(user.createdAt).toBeTruthy();
    expect(user.updatedAt).toBeTruthy();
  });

  it('should update the updatedAt field after an update (with the save method of the service)', async () => {
    const user = service.create(testUser);
    expect(user.updatedAt).not.toBeTruthy();

    await service.save(user);
    expect(user.updatedAt).toBeTruthy();
    expect(user.createdAt.getTime()).toBe(user.updatedAt.getTime());

    const actualUpdate = user.updatedAt;
    user.email = 'test2@test2.com';
    expect(user.updatedAt.getTime()).toBe(actualUpdate.getTime());

    await service.save(user);
    await wait(20); // <-- this is just to simulate an update after "some time"
    expect(user.updatedAt.getTime()).toBeGreaterThan(actualUpdate.getTime());
  });
});

Now let's see what we have:

We have our test module for the DB
const module: TestingModule = await Test.createTestingModule({
  imports: [
    DbModule({
      name: (new Date().getTime() * Math.random()).toString(16),
    }),
    TypeOrmModule.forFeature([
      UserEntity,
    ]),
  ],
  ....
}).compile();

As you can see, we define a parameter name with some random values; We are giving a name to our connection and that way we avoid issues with duplicated names in our tests. At the same time this module give us the access to MongoDB so we can use it just like we do in our normal (non-testing) flow.

Then we define our Entity normally because we are using a real Database so in our application this works like a non-test environment.

But... Why?

The reason of this solution comes in this block of code (and the next one too):

it('should save the user and add the createdAt and savedAt fields', async () => {
  const user = service.create(testUser);
  await service.save(user);

  expect(user.createdAt).toBeTruthy();
  expect(user.updatedAt).toBeTruthy();
});

Here we are fully testing our model because the fields createdAt and updatedAt only will be in our object after we save it with our service... In theses cases were we want a full test is where this method shines.

Last words

As always in software, this is not the perfect solution for all the cases, we always need to understand how our site/flow works and then we will know what is the best in our process.

I use this method for end-2-end testing and for unit testing I only mock some basic functionality of the models.

Happy coding!

Comments (3)

Victor Assis's photo

Hi Enrique, thanks for your approach. Im trying to do the same, but using @nestjs/mongoose. You faced problems of the server is still on after tests finished?

When I'm running Jest in a specific file, I'm get this log from Jest and command doesn't exit:

Jest did not exit one second after the test run has completed.

This usually means that there are asynchronous operations that weren't stopped in your tests. Consider running Jest with `--detectOpenHandles` to troubleshoot this issue.

When I'm running Jest for all project, the command exit, but jest log:

A worker process has failed to exit gracefully and has been force exited. This is likely caused by tests leaking due to improper teardown. Try running with --runInBand --detectOpenHandles to find leaks.
Enrique Arrieta's photo

Hi Victor, with the mongoose package is almost the same configuration (I'm currently using it).

Could you provide an example of your test? That way is easier for me to try to help but the first error usually happens when there is an async process but Jest is not able to know when the test ends.

Victor Assis's photo

Enrique Arrieta I managed to solve by closing the connection with Mongoose and ending the server in afterEach.

Here is my code: github.com/nestjs/mongoose/issues/167#issue..