DEV Community

Cover image for Unit testing NestJS with mongo in memory...
Julien Prugne for Webeleon

Posted on • Updated on

Unit testing NestJS with mongo in memory...

Assuming you already installed and configured mongoose in your NestJS project.

For the sake of having something to test we will create a Squid API. The API will provide a random squid gif when called.
You can see the actual implementation in the demo repo.

Writing tests for code that interact with databases is rather painful.

You either have to create test databases and delete them afterward.
OR
You end up writing and debugging a ton of code to clean before after the testing...

Today is the end of your misery!
I am here to save you the trouble of testing. with nestJS, mongoose and MongoDB.... sorry for the others

First, we will need to add a new development package to the project. (link to the Github repository provided at the end of this article)

npm i --save-dev mongodb-memory-server 
Enter fullscreen mode Exit fullscreen mode

Cool, We can now spawn mongo daemon in memory! How awesome is that?
Since I am a lazy brat, I do not want to rewrite the in-memory mongod bootstrapping code.
Let's write a small test utils file that will provide us an easy to import preconfigured root MongooseModule and an helper to close the connection.

import { MongooseModule, MongooseModuleOptions } from '@nestjs/mongoose';
import { MongoMemoryServer } from 'mongodb-memory-server';

let mongod: MongoMemoryServer;

export const rootMongooseTestModule = (options: MongooseModuleOptions = {}) => MongooseModule.forRootAsync({
  useFactory: async () => {
    mongod = new MongoMemoryServer();
    const mongoUri = await mongod.getUri();
    return {
      uri: mongoUri,
      ...options,
    }
  },
});

export const closeInMongodConnection = async () => {
  if (mongod) await mongod.stop();
}
Enter fullscreen mode Exit fullscreen mode

Excellent, in-memory plug an play MongoDB daemon!
Let's import that bad boy to our service and controller test.
Don't forget to close the connection in the afterAll function.

import { Test, TestingModule } from '@nestjs/testing';
import { MongooseModule } from '@nestjs/mongoose';

import { SquidService } from './squid.service';
import { closeInMongodConnection, rootMongooseTestModule } from '../test-utils/mongo/MongooseTestModule';
import { SquidSchema } from './model/squid.schema';

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

  beforeEach(async () => {
    const module: TestingModule = await Test.createTestingModule({
      imports: [
        rootMongooseTestModule(),
        MongooseModule.forFeature([{ name: 'Squid', schema: SquidSchema }]),
      ],
      providers: [SquidService],
    }).compile();

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

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

  /**
    Write meaningful test
  **/

  afterAll(async () => {
    await closeInMongodConnection();
  });
});
Enter fullscreen mode Exit fullscreen mode

And voila!
You are all set.
Go back to testing the wonderful code you are writing!

Next Time we will handle the case of end to end test for NestJS.

Buy Me A Coffee

Sources

NestjJS
NestJS techniques mongodb
mongod-in-memory
The issue that saved me

Discussion (18)

Collapse
chomikpawel profile image
Pawel Chomiczewski

Hello there,

I write down everything as you posted but I still get warning in jest:
"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 --detectOpenHandles to find leaks."
Do you know how to fix it?

Collapse
bassochette profile image
Julien Prugne Author

Hi Pawel,

Sometimes I have this message. I tweaked a bit by test command with a few options.

"test": "jest --force-exit --detectOpenHandles",
"test:watch": "jest --watch --detectOpenHandles",
"test:cov": "jest --coverage --force-exit --detectOpenHandles",
Enter fullscreen mode Exit fullscreen mode

--detectOpenHandles should help you find the non-closing process. It might also remove the message

--force-exit will make sure to kill the process and allow your test to run smoothly on CI environments. It is a bit barbaric but it gets the job done.

I've done a few tests before replying. The first screenshot does not use the --detectOpenHandles options and the seconds do.

"test:watch": "jest --watch",
without --detectOpenHandles

"test:watch": "jest --watch --detectOpenHandles",
with --detectOpenHandles

Collapse
chomikpawel profile image
Pawel Chomiczewski • Edited

Hello, thank you for your answer but those flags unfortunately don't turn off those warnings but I just wanted to know if those warnings mean anything meaningful or should I just brush it off? Cheers!

Edit. I'm fairly new to Node/Nest stuff, could you please also tell me how can I add helper method to this utility to clear all collections? Cheers!

Thread Thread
bassochette profile image
Julien Prugne Author • Edited

You don't need to clear collection between tests since it's a new in-memory mongo.

during the test I do something like that:

beforeEach(async () => {
      const myModel = module.get<Model<someDocument>>('MyModel')
      await myModel.deleteMany({});
});
Enter fullscreen mode Exit fullscreen mode

like the service in the test, the module needs to be accessible.

Thread Thread
chromey profile image
Christian Romeyke • Edited

Could you maybe provide a full example of that? I tried to get this working in my own repo and in yours, using the Squid model, but got an error in both cases: Nest could not find Squid element (this provider does not exist in the current context). Much appreciated!

Thread Thread
bassochette profile image
Julien Prugne Author

You can check my Colonie repo. The pvp-shield module have the in memory db, collection deletion between it and the way to get the model from the testing module.
github.com/bassochette/colonies/tr...

Thread Thread
chromey profile image
Christian Romeyke

Thanks a lot, I managed to do it! For others who might struggle: I wasn't aware of the convention for the model name to use in module.get(). It's the name property that gets passed into MongooseModule.forFeature() + the suffix 'Model' (SquidModel in this example). Apologies if this obvious, but this string is never explicitly defined as a constant or class name etc.

Thread Thread
bassochette profile image
Julien Prugne Author

Oh, that's really not obvious...
I found this trick by mimicking the underlying metod of InjectModel from the nest/mongoose repo.
github.com/nestjs/mongoose/blob/28...

Collapse
jrrmcalcio profile image
Julio

Hi guys, there were some changes on mongo-memory-server so I paste here the updated class with version 7:

import { MongooseModule, MongooseModuleOptions } from '@nestjs/mongoose';
import { MongoMemoryServer } from 'mongodb-memory-server';
import { disconnect } from 'mongoose';

let mongo: MongoMemoryServer;

export const rootMongooseTestModule = (options: MongooseModuleOptions = {}) =>
  MongooseModule.forRootAsync({
    useFactory: async () => {
      mongo = await MongoMemoryServer.create();
      const mongoUri = mongo.getUri();
      return {
        uri: mongoUri,
        useCreateIndex: true,
        ...options,
      };
    },
  });

export const closeMongoConnection = async () => {
  await disconnect();
  if (mongo) await mongo.stop();
};
Enter fullscreen mode Exit fullscreen mode
Collapse
deejoe79 profile image
deejoe79

Thank you for the article, this was very helpful. I also got the "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."

The advice was not practical for me because in VSCode I often run 1-2 tests only (using the extension: Jest Runner).

I share what worked for me. Go to the MongooseTestModule.ts (typescript please :)
add:

import mongoose from 'mongoose';
Enter fullscreen mode Exit fullscreen mode
export const closeInMongodConnection = async () => {
    await mongoose.disconnect();
    if (mongod) await mongod.stop();
};
Enter fullscreen mode Exit fullscreen mode
Collapse
bassochette profile image
Julien Prugne Author

Thanks for you help ❤
I'll try to update the post and the sample code repo on github.

Collapse
skuarchdevelop profile image
Skuarch

thanks for your article it really help me

when jest doesn't finish the execution try to use this in afterAll

afterAll(async () => {
await closeInMongodConnection();
mongoose.disconnect();
mongoose.connection.close();
});

Collapse
bassochette profile image
Julien Prugne Author • Edited

I'll try to update the post and sample code on github soon :)
Thanks for your help.
@deejoe79 offered a similar solution in the shutdown function helper.

Collapse
robsonpca01 profile image
Robson Carvalho

Good article! In my scenario, i have two services, each with its own schema, but both schemas have relationship with each other, do you have any suggestion about how can i setup this tests? I just want to use mongoose populate function to get the related data.

thnks!

Collapse
bassochette profile image
Julien Prugne Author

Using an in-memory MongoDB will not change the behavior of mongoose. Therefore, if the link data are seeded in the database you will be able to use the populate method.

Now what you might struggle with is: How to seed?
I see to path for you:

1: use the services.
they need to be available as providers in your testingModule.
The caveat here is: you won't be able to test independently both services.

2: get the models from the testingModule

You'll be able to bypass any logic and just add data for your test.
The caveat here: you are bypassing all business logic...

here is a little example:

import { Test, TestingModule } from '@nestjs/testing';
import { MongooseModule } from '@nestjs/mongoose';
import { Model } from 'mongoose';

import { IngredientDocument } from 'path to the interface or class';
import { RecipeDocument } from 'path to the interface or class';
import { IngredientSchema } from 'path to the schema';
import { RecipeSchema } from 'path to the schema';

import { RecipeService } from 'path to the recipe service';

describe('RecipeService', () => {
  let recipeService: RecipeService;

  let testingModule: TestingModule;

  let ingredientModel: Model<IngredientDocument>;
  let recipeModel: Model<RecipeDocument>;

  beforeEach(async () => {
    testingModule = await Test.createTestingModule({
      imports: [
        rootMongooseTestModule(),
        MongooseModule.forFeature([

          // use the same model name as you declared them in your module

          { name: 'Recipe', schema: RecipeSchema },
          { name: 'Ingredient', schema: IngredientSchema },
        ]),
      ],
      providers: [RecipeService],
    }).compile();

    recipeService = testingModule.get<RecipeService>(RecipeService);

    // The string to get the model is: 'token used as name in Mongoose.forFeature' + 'Model'
    recipeModel = testingModule.get<Model<RecipeDocument>>(
      'RecipeModel',
    );

    // The string to get the model is: 'token used as name in Mongoose.forFeature' + 'Model'
    ingredientModel = testigModel.get<Model<IngredientDocument>>(
      'IngredientModel',
    );
  });

  afterAll(async () => {
    await closeInMongodConnection();
  });

  afterEach(async () => {
    await recipeModel.deleteMany({});
    await ingredientModel.deleteMany({});
  });

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

  // write your test here

});

Enter fullscreen mode Exit fullscreen mode

hoping this will help you start :)

Collapse
tylersustare profile image
Tyler Sustare

This was super helpful! Thank you 🎉🥳

Collapse
bassochette profile image
Julien Prugne Author

Happy to help ;)

Collapse
rogeliosamuel621 profile image
rogeliosamuel621

Really helpful, you make my life easier :)