Last few weeks I’ve been working on creating Unit Tests for a Node.js and Mongoose application where most of the logic is handled by mongoose and MongoDB.
The first thing I tried was to create mocks to match every operation executed in mongoose and its different outcomes (at first it looked like the most logical thing to do). But half through the process I started to realize it was taking a lot of time, and what if the queries change? Will I have to change all my mocks as well?
After googling for a while I found this package on Github mongodb-memory-server which, simply put, allows us to start a mongod
process that stores the data in memory. So I decided to give it a try.
In this article I’ll tell you how to use an in-memory MongoDB process to test your mongoose logic without having to create any mocks. If you want to go straight to the code, I created a Github repo that serves as example or boilerplate.
In-memory database pros & cons
I wasn’t convinced about using an in-memory database instead of mocks at first so I did a bit of digging and come up with this list of pros an cons:
Pros:
- No need for mocks: Your code is directly executed using the in-memory database, exactly the same as using your regular database.
- Faster development: Given that I don’t need to build a mock for every operation and outcome but only test the query, I found the development process to be faster and more straightforward.
- More reliable tests: You’re testing the actual code that will be executed on production, instead of some mock that might be incorrect, incomplete or outdated.
- Tests are easier to build: I’m not an expert in unit testing and the fact that I only need to seed the database and execute the code that I need to test made the whole process a lot easier to me.
Cons:
- The in-memory database probably needs seeding
- More memory usage (dah)
- Tests take longer to run (depending on your hardware).
In conclusion, the in memory database turned out to be perfect to test applications where the logic is mainly handled through database operations and where the memory and execution time are not an issue.
Let’s start coding!
In this example we’ll create a mongoose schema and a service that executes some operations with that schema. We will later test the operations executed by the service.
This is how our project will look like once we finish:
1. Setup & Install dependencies
Run npm init
to setup your project, don’t worry about the test script yet, will take care of it later.
And then execute the following commands to install all dependencies:
npm install --save mongoose
npm install --save-dev jest mongodb-memory-server
Note: When installing
mongodb-memory-server
the mongod binaries will be downloaded an installed innode_modules/.cache
. There are other options you can try likemongodb-memory-server-global
which will download the binaries in%HOME/.cache
so they’ll be available to test other projects. Ormongodb-memory-server-core
which will only download the binaries on server start if it can’t find them.Pick the option that best suits your needs.
More info in github.com/nodkz/mongodb-memory-server.
2. Write code to test
Now we’ll build the model schema and the service that we’ll test later.
2.a Product schema
// src/models/product.js
const mongoose = require('mongoose');
/**
* Product model schema.
*/
const productSchema = new mongoose.Schema({
name: { type: String, required: true },
price: { type: Number, required: true },
description: { type: String }
});
module.exports = mongoose.model('product', productSchema);
2.b Product service
// src/services/product.js
const productModel = require('../models/product');
/**
* Stores a new product into the database.
* @param {Object} product product object to create.
* @throws {Error} If the product is not provided.
*/
module.exports.create = async (product) => {
if (!product)
throw new Error('Missing product');
await productModel.create(product);
}
3. Configure jest
First, we’ll add the test
script to the package.json
:
"scripts": {
"test": "jest --runInBand ./test"
}
Note: The
--runInBand
parameter will make sure all tests run serially. I do this to make sure there’s only one mongod server running at once.
And finally add this to your package.json
, since we are running a node application.
"jest": {
"testEnvironment": "node"
}
4. In-memory database handling
I wrote a module that executes some basic operations that I’ll use to handle the in-memory database.
// tests/db-handler.js
const mongoose = require('mongoose');
const { MongoMemoryServer } = require('mongodb-memory-server');
const mongod = new MongoMemoryServer();
/**
* Connect to the in-memory database.
*/
module.exports.connect = async () => {
const uri = await mongod.getConnectionString();
const mongooseOpts = {
useNewUrlParser: true,
autoReconnect: true,
reconnectTries: Number.MAX_VALUE,
reconnectInterval: 1000
};
await mongoose.connect(uri, mongooseOpts);
}
/**
* Drop database, close the connection and stop mongod.
*/
module.exports.closeDatabase = async () => {
await mongoose.connection.dropDatabase();
await mongoose.connection.close();
await mongod.stop();
}
/**
* Remove all the data for all db collections.
*/
module.exports.clearDatabase = async () => {
const collections = mongoose.connection.collections;
for (const key in collections) {
const collection = collections[key];
await collection.deleteMany();
}
}
5. Write some tests
And finally we test our product service with the following code:
// tests/product.test.js
const mongoose = require('mongoose');
const dbHandler = require('./db-handler');
const productService = require('../src/services/product');
const productModel = require('../src/models/product');
/**
* Connect to a new in-memory database before running any tests.
*/
beforeAll(async () => await dbHandler.connect());
/**
* Clear all test data after every test.
*/
afterEach(async () => await dbHandler.clearDatabase());
/**
* Remove and close the db and server.
*/
afterAll(async () => await dbHandler.closeDatabase());
/**
* Product test suite.
*/
describe('product ', () => {
/**
* Tests that a valid product can be created through the productService without throwing any errors.
*/
it('can be created correctly', async () => {
expect(async () => await productService.create(productComplete))
.not
.toThrow();
});
});
/**
* Complete product example.
*/
const productComplete = {
name: 'iPhone 11',
price: 699,
description: 'A new dual‑camera system captures more of what you see and love. '
};
There are more test examples on the repo in case you want to check them out.
6. Try it out!
To try out our new tests just run npm test
in the terminal 👩💻 and watch your tests come to life!