Writing unit tests for MongoDB and Mongoose

Introduction

Over the past couple of days, I have been working to complete freeCodeCamp‘s issue tracker project. I’m using MongoDB as the database and relying on Mongoose to model the data. Since I have been learning about testing lately, I wanted to write unit tests for the simple wrapper I wrote around Mongoose to add, update and remove issues to the database.

This was the first time for me writing tests for MongoDB or any other database. The previous project in the Quality Assurance module didn’t require setting up a database at all.

To write these tests, I used Mocha, one of the available testing frameworks for Javascript, along with the Chai Assertion Library.

First version of the test

I went through a couple of versions of the tests until I was satisfied with the result.

In this post, I will focus on the function that adds issues to the database. It was the first function I implemented, so its tests were the ones that saw the most change. The function itself didn’t change at all as it wasn’t really a complex one. This is how I implemented it:

module.exports = {
    addIssue: function (issueObj) {
        const doc = new Issue({
            issue_title: issueObj.isuue_title,
            issue_text: issueObj.issue_text,
            created_by: issueObj.created_by,
            project: issueObj.project,
            assigned_to: issueObj.assigned_to,
            status_text: issueObj.status_text,
        });

        return doc.save();
    }
};

As you can see, it is a very simple function. Some might argue that it doesn’t even need much testing, but I still wanted to write unit tests for it to practice.

I am using Mocha’s Behavior-Driven Development (BDD) interface. Here is how I set up the test suite:

describe("Unit Tests", function () {
    describe("Database operations", function () {
        describe("addIssue", function() {

        });
    });
});

Then, inside the most inner describe() block, I add the following test, which ensures that the addIssue function successfully saves the issue to the database. This is how it works:

it("should insert an issue to the database with required fields completed", function (done) {
    mongoose
        .connect(process.env.MONGO_URL, {
            useNewUrlParser: true,
            useUnifiedTopology: true,
            useCreateIndex: true,
        })
        .then(() => {
            const issue = {
                issue_title: "New issue",
                issue_text: "Issue description",
                created_by: "John Doe",
                project: "test",
            };

            dbOp.addIssue(issue).then((doc) => {
                assert.containsAllKeys(doc["_doc"], issue);
                assert.equal(doc.issue_title, issue.issue_title);
                assert.equal(doc.issue_text, issue.issue_text);
                assert.equal(doc.created_by, issue.created_by);
                assert.equal(doc.project, issue.project);

                done();
            });
        })
        .catch((err) => {
            console.log(err);

            assert.fail();

            done(err);
        });
});

First, I set up the connection to the database. Then, I call the addIssue function and use the Chai Assertion Library to test the returned document.

I used 2 types of assertions here. The first checks that the returned document contains all the keys it is supposed to have.

assert.containsAllKeys(doc["_doc"], issue);

The second checks that the values of each of those keys is exactly what they should be.

assert.equal(doc.issue_title, issue.issue_title);

However, there are 2 problems with the way I set up the tests there.

Problem One: Repetitive connection to the database

The way I wrote this first version of the tests means that I will have to connect to the database multiple times. I wanted to find a solution to this, so I could just set up the connection once and run all the tests. The solution for this problem turned out to be very simple, hooks.

Hooks are special functions provided by Mocha to write any general setup required for your tests. In the BDD interface, there are 4 different hooks:

  1. before(): Any code included in this hook runs once before the first test in the suite.
  2. beforeEach(): Any code included here runs before each test in the suite.
  3. after(): Any code included here runs once after the last test in the suite.
  4. afterEach(): Any code included here runs after each test in the suite.

In my case, the before is what I needed. I moved the database connection there. This is how the code looks like after that:

describe("Unit Tests", function () {
    describe("Database operations", function () {
        before(function(done) {
            mongoose
                    .connect(process.env.MONGO_URL, {
                        useNewUrlParser: true,
                        useUnifiedTopology: true,
                        useCreateIndex: true,
                    })
                    .then(() => done())
                    .catch((err) => {
                        console.log(err);

                        assert.fail();

                        done(err);
                    });
        });

        describe("addIssue", function() {
            it("should insert an issue to the database with required fields completed", function (done) {
                const issue = {
                    issue_title: "New issue",
                    issue_text: "How to solve this?",
                    created_by: "Abdelrahman Said",
                    project: "test",
                };
                
                dbOp.addIssue(issue)
                    .then((doc) => {
                        assert.containsAllKeys(doc["_doc"], issue);
                        assert.equal(doc.issue_title, issue.issue_title);
                        assert.equal(doc.issue_text, issue.issue_text);
                        assert.equal(doc.created_by, issue.created_by);
                        assert.equal(doc.project, issue.project);

                        done();
                    })
                    .catch((err) => {
                        console.log(err);

                        assert.fail();

                        done(err);
                    });           
            });
        });
    });
});

Problem Two: The tests affect the actual database

The second issue I had was that the tests were actually connecting to the real database. This means that each time I run the tests, I would be adding unnecessary data to the database.

I looked online and came across an NPM package called mockgoose. This package mocks a MongoDB database in memory, so you can run tests on this database without actually affecting your production database.

However, this package is actually deprecated now, and its creators recommend using the mongodb-memory-server package instead.

To use this package, I needed to initialise it first. Here is how you can do it:

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

const mongoServer = new MongoMemoryServer();

Next, since I am using Mongoose, I connect Mongoose to the database in memory instead of the production database. This happens inside the before hook as mentioned before. Here is the code before (left) and after (right) the change.

before(function (done) {
    mongoose
        .connect(process.env.MONGO_URL, {
            useNewUrlParser: true,
            useUnifiedTopology: true,
            useCreateIndex: true,
        })
        .then(() => done())
        .catch((err) => {
            console.log(err);

            assert.fail();

            done(err);
        });
});




before(function (done) {
    mongoServer
        .getUri()
        .then((mongoURI) => {
            mongoose
                .connect(mongoURI, {
                    useNewUrlParser: true,
                    useUnifiedTopology: true,
                    useCreateIndex: true,
                })
                .then(() => done());
        })
        .catch((err) => {
            console.error(err);

            assert.fail();

            done(err);
        });
});

The main difference here is that I added a call to mongoServer.getUri() which returns a URI to the database in memory. I then pass that URI to the mongoose.connect function. Now, all database operations that happen in the unit tests don’t affect the production database.

Conclusion

Using these simple techniques, I managed to improve the unit tests I wrote. I think the result is better than the initial tests I wrote. However, I am still learning, of course, so any feedback would be very welcome.