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
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: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)
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).
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
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:
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
which I guess should be the same version.
So what am I missing? I would be very happy about some hint!
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 ;-)Thanks for the article, I’m new to Mongodb, do you think Mongodb is suitable for a full fledged Social Network
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.