Codementor Events

Speed Up Your MongoDB Tests in Node

Published Apr 11, 2020Last updated Oct 13, 2020
Speed Up Your MongoDB Tests in Node

You've got a couple of choices when it comes to backend tests that interact with mongo: mock the calls or query a live database. The former has the serious disadvantage of potential inaccuracy (e.g. there could be typos in your selectors). The latter can be impractically slow when running a large number of tests.

The best solution I've found is programmatically spin up an in-memory real mongodb instance for each group of tests, using the mongodb-memory-server package.

npm i -D mongodb-memory-server

Alternatively, if you want to share the same mongodb binary for all of your projects (this is what I prefer) you can run:

npm i -D mongodb-memory-server-global

If you read through the docs, you'll that see it works in numerous testing environments, and with exiting ODMs like mongoose. Additionally, it will download the necessary mongodb binary for you, so there's no external setup required.

In the remainder of this article, I'll explain how I use mongodb-memory-server along with the native mongodb driver. Finally, we'll wrap up with a simple Jest example.

Here's my implementation of a class called DBManager which encapsulates all of the DB setup and teardown functionality:

const { MongoClient } = require('mongodb');
const { MongoMemoryServer } = require('mongodb-memory-server-global');

// Extend the default timeout so MongoDB binaries can download
jest.setTimeout(60000);

// List all of your collection names here - I'll add some examples
const COLLECTIONS = ['users', 'groups', 'comments'];

class DBManager {
  constructor() {
    this.db = null;
    this.server = new MongoMemoryServer();
    this.connection = null;
  }

  // Spin up a new in-memory mongo instance
  async start() {
    const url = await this.server.getUri();
    this.connection = await MongoClient.connect(url, {
      useNewUrlParser: true,
      useUnifiedTopology: true,
    });
    this.db = this.connection.db(await this.server.getDbName());
  }

  // Close the connection and halt the mongo instance
  stop() {
    this.connection.close();
    return this.server.stop();
  }

  // Remove all documents from the entire database - useful between tests
  cleanup() {
    return Promise.all(COLLECTIONS.map((c) => this.db.collection(c).deleteMany({})));
  }
}

module.exports = DBManager;

And here's a simple Jest test suite using a DBManager instance:

const DBManager = require('./DBManager');

const GROUP = { _id: 'g1', createdAt: new Date() };

describe('Group.getGroupById', () => {
  const dbman = new DBManager();

  afterAll(() => dbman.stop());
  beforeAll(() => dbman.start());
  afterEach(() => dbman.cleanup());
  beforeEach(() => dbman.models.Group.collection.insertOne(GROUP));

  it('should return null with an invalid id', async () => {
    expect.assertions(1);
    const result = await dbman.models.Group.getGroupById('x');
    expect(result).toBeNull();
  });

  it('should return a group with a valid id', async () => {
    expect.assertions(1);
    const result = await dbman.models.Group.getGroupById(GROUP._id);
    expect(result).toEqual(GROUP);
  });
});

The details of the tests aren't that important, but note how we spin up a mongodb instance at the start of the test block with beforeAll, and then stop it at the end of the test block with afterAll. For each test in the block, we keep the same database running, but remove all of its contents with dbman.cleanup(). This ensures each test runs in a clean environment.

Note the above implementation requires that all available collections are listed in the COLLECTIONS array. A more sophisticated approach would be to query the database for all collections at the time of removal. There are packages that do precisely this.

At the time of this writing, I have 712 backend tests (98% of which require the database), and the total execution time is 64.7 seconds on my aging i5-2400.

Enjoy the speed increase!

Photo by Veri Ivanova on Unsplash

Discover and read more posts from David Weldon
get started
post comments5Replies
icetbr
4 years ago

Hi, nice article. I would just like to remind people a few things regarding mongodb-memory-server, please someone correct me if I’m wrong:

  1. it’s faster (~3x) for a battery of parallel tests, because when you use a real database you’re usually constrained to one process (https://github.com/nodkz/mongodb-memory-server/issues/105)

  2. it’s slower (~4x) for one off tests, because it has to create the database engine every time. This is how I code, I always have one test running many times while TDDing (based on my own testing).

  3. it is has somewhat the same speed in the other cases (based on my own testing). Please remember that mongo >= 3.2 runs with a memory cache by default.

https://docs.mongodb.com/manual/core/wiredtiger/#memory-use

Frederik Held
4 years ago

Hey David, thanks for the great article! I have an issue with mongodb-memory-server that I can’t wrap my head around: it doesn’t seem to deal with unique keys properly. Maybe you can give me a hint with that?

This is my code:

// ./model/thing.js

const mongoose = require('mongoose')

const thingSchema = new mongoose.Schema({
  name: {
    type: String,
    required: true,
    unique: true,
    dropDups: true
  }
})

module.exports = mongoose.model('Thing', thingSchema)
// ./service/thing.js

const ThingModel = require('../models/thing')

const thingService = { }

thingService.create = async (thing) => {
  const thingModel = new ThingModel(thing)
  try {
    const result = await thingModel.save()
    return result
  } catch (error) {
    console.log('error', error)
    return { error: error }
  }
}

module.exports = thingService

If I run plantService.create({ name: 'foo' }) against a dockerized MongoDB “mongo:4.2-bionic” instance, it correctly gives me error code “11000” if I try to insert a thing with the same name twice.

However, if I do exactly the same in my test suite against mongodb-memory-server, it inserts two instances with the same name without any error.

I use

process.env.MONGOMS_DOWNLOAD_URL = 'https://fastdl.mongodb.org/linux/mongodb-linux-x86_64-ubuntu1804-4.2.8.tgz'
process.env.MONGOMS_VERSION = '4.2.8'

which I guess should be the same version.

So what am I missing? I would be very happy about some hint!

Frederik Held
4 years ago

It’s funny, how I can spend a whole day without seeing any light, but as soon as I ask someone for help, the solution shows up :-D

I found this issue in the mongodb-memory-service repo: https://github.com/nodkz/mongodb-memory-server/issues/102

This answer fixes my issue: https://github.com/nodkz/mongodb-memory-server/issues/102#issuecomment-448774442

I put await ThingModel.ensureIndexes() right before my tests / right after the db was populated with test data. Now it works :-) Thanks for being my rubber duck ;-)

Sheldon
5 years ago

Thanks for the article, I’m new to Mongodb, do you think Mongodb is suitable for a full fledged Social Network

David Weldon
5 years ago

It’s hard to answer in the general case. I find mongo very easy to get started with because you don’t have to spend time pondering your schema, and there’s usually little impedance mismatch between the data stored and the data required on your client. Over the long run, however, if you have relational data it’s often more convenient to store in a way that can enforce (and query) those relationships.

Show more replies