#100DaysOfMERN - Day 35

#100DaysOfMERN - Day 35

·

10 min read

✏ How to test your MongoDB/Mongoose backend code with Mocha

Mocha is a free testing framework that runs both in Node and in the browser. You can use it to test pretty much anything, but specifically in combination with MongoDB, it allows you to test all the code that handles CRUD operations on the database in isolation - that is, independent of the frontend or the server.

✏ Getting started

First, create a test folder (it has to have that name, so Mocha can find your test files). Within it, I'll create a file math_test.js just to explain the basic functionality:

Installing Mocha:

npm i mocha

And adding a script to the package.json:

"scripts": {
        "test": "mocha"
    }

In math_test.js, you don't have to import Mocha, but the assert package. A test is then written using a method describe, which takes a string as first argument to describe what this test is supposed to do, and a callback function (note that the use of arrow functions is discouraged in Mocha):

test/math_test.js

const assert = require('assert');

// describes a whole block of tests
describe('a test if JS can do basic math', function () {

});

To specify what we want to test, we now need one or more it blocks, with each of them defining a certain test. Like describe, it takes a string with a description of the details for the test, and a callback. Finally, within the callback, we use assert to define what should be the result of the test.

For example, if we want to test if JavaScript is able to correctly calculate 1+1 and 1*1:

const assert = require('assert');

// describes a whole block of tests
describe('a test if JS can do basic math', function () {

    // describe a single test
    it('correctly calculates 1 + 1', function () {

        // define what the result should be
        assert(1 + 1 === 2);
    });

    // describe another test
    it('correctly calculates 1 * 1', function () {
        assert(1 * 1 === 1);
    });

});

Running the test now (Mocha will look into the test folder and run every file it finds in there, which at the moment is only one):

npm run test

This gives the following feedback in the console:

mocha-example.jpg

If one or both of those tests fail, you'll get a big red error instead, and also detailed information on which part was failing.

This is basically how Mocha works. The test is utterly stupid, but hopefully it illustrated what's going on - so now, I'm moving on to a more realistic example:


✏ Setup for a real Project

I have a backend folder of a simple game I'm writing. At the end of the game, players can enter a name and submit their highscore, which will be stored in a database. So the first thing to do is create a Model for the collection and export it:

models/scoremodel.js

const mongoose = require('mongoose');
const Schema = mongoose.Schema;

const ScoreSchema = new Schema(
    {
        name: String,
        highscore: Number
    },
    { timestamps: true }
);

const Score = mongoose.model('score', ScoreSchema);

module.exports = Score;

Connecting to the database

As Mocha will run every file inside the test folder, I'll put a script in there to connect to a test database called testdb, which has a (so far) empty scores collection in it:

connect.js

const mongoose = require('mongoose');
const dotenv = require('dotenv');
dotenv.config();

const connStr = process.env.MONGODB_URL;
const connOptions = {
    useNewUrlParser: true,
    useUnifiedTopology: true,
    useFindAndModify: false,
    useCreateIndex: true
};

mongoose
    .connect(connStr, connOptions)
    .then(() => {
        console.log('Connected to Database');
    })
    .catch(err => console.log(`Error in connect.js: ${err}`));

✏ First Test: Saving to the Database

I'll rename that math_test.js file now to save_test.js and make a few changes. First of all, require the Score Model, and leave the describe/it skeleton and add new descriptions.

I already know that in order to save a record, I'll have to create a new instance of my Model, and then (quite simply) use its .save method. Mongoose already knows where to save it, because of this line in scoremodel.js:

const Score = mongoose.model('score', ScoreSchema);

It'll take the name of the model and pluralise it, to get the collection name (scores).

So, creating a variable newScore and saving it would work like this:

const assert = require('assert');
const Score = require('../models/scoremodel');

describe('saving records to the database', function () {

    it('saves a record to the scores collection', function () {

        // create newScore
        const newScore = new Score({
            name: 'jsdisco',
            highscore: 9001
        });

        // save to DB
        newScore.save()
    });

});

This still lacks the assert though. In order to determine whether saving to the DB has worked, Mongoose has a method isNew which returns true if an instance of a Model wasn't saved to the DB yet, and false otherwise. Using this:

const assert = require('assert');
const Score = require('../models/scoremodel');

describe('saving records to the database', function (done) {

    it('saves a record to the scores collection', function () {

        // create newScore
        const newScore = new Score({
            name: 'jsdisco',
            highscore: 9001
        });

        // save to DB
        newScore.save()
            .then(function () {
                // assert that .isNew returns false
                assert(!newScore.isNew);
                done();
        });
    });

});

Because .save is an async function, Mocha can't tell when the test is done. I can however pass a function done, and invoke it right after assert, to explicitly tell Mocha that it can proceed to possible subsequent tests.

Running the test now:

mocha-example-2.jpg

Works, yay. There is still a flaw in this though, because things are slightly out of order. From the logs, you can see that first Mocha starts with the test "saving records to the database", and after that, my connection script logs "Connected to the database". Ideally, I'd want to make sure that my connection is working first, before any tests even start to run.


✏ Mocha Hooks

Mocha offers four different hooks to define the order in which code should run:

  • before(): runs once before the first test

  • after(): runs once after the last test

  • beforeEach(): runs before each test

  • afterEach(): runs after each test

(Each test corresponds to one it block, not a whole file)

To make sure that the connection is made before any tests run, I'll wrap the connect part in a before hook (passing done again and calling it after the connection was made):

connect.js

before(function (done) {
    mongoose
        .connect(connStr, connOptions)
        .then(() => {
            console.log('Connected to Database');
            done();
        })
        .catch(err => console.log(`Error in connect.js: ${err}`));
});

Now, stuff appears in order:

mocha-example-3.jpg

I want to add one more thing though, using the after() hook: Once all the tests are done, I'd like to close the connection, and stop the script:

after(function () {
    mongoose.connection.close();
    process.exit(0);
});

✏ Cleaning up before each Test

When writing tests, it's a good idea to make sure that each test runs "isolated" and that the tests are independent of each other. At the moment, my test adds a new score to the database each time, so I've already ~10 score records in there by now, all with the same name.

Ideally, I'd start with a cleaned up database for each test, so I'll add another hook that drops the collection first:

connect.js

beforeEach(function (done) {
    mongoose.connection.collections.scores.drop(function () {
        done();
    });
});

Running npm run test again - and now, I have only one user in the database, the one I'm creating with my test, and that gets dropped before any new test.


✏ Second Test (2-a): Finding Records in the Database

As this is a different category of test, I'll create a new file find_test.js, which will look very similar to the other test file - the basic syntax is the same, it'll have a describe method and within it, a number of it blocks.

Since I'm dropping the collection before each test, and trying to find something in a non-existent collection makes little sense, I'll now first add a new record to it within a beforeEach hook. I don't have to check if that works, because the save_test.js has already confirmed that it does.

You might wonder why I'm first dropping the collection and all its records before each test, and then add a record again within find_test.js. Why not use the other test in save_test.js, which already does that for me? The reason is that you don't want to layout your tests in a way that their outcome depends on the order in which they run. Tests should never interfere with each other, and should be isolated from each other.

The test here is for the findOne method. mongoose.findOne({ condition }) returns the first match it found, which I'll then use in the assert part:

find_test.js

const assert = require('assert');
const Score = require('../models/scoremodel');

describe('finding records from the database', function () {

    // adding a score record to the empty collection
    beforeEach(function (done) {
        const newScore = new Score({
            name: 'jsdisco',
            highscore: 9001
        });

        newScore.save().then(function () {
            done();
        });
    });


    // test 2-a starts here
    it('finds one record from the scores collection', function (done) {
        Score.findOne({ name: 'jsdisco' }).then(function (result) {
            assert(result.name === 'jsdisco');
            done();
        });
    });
});

And the result shows:

mocha-example-4.jpg

The order of these is determined by the order in which they appear in the test folder, so essentially by the filename. That's another reason why you want to make sure that the order absolutely doesn't matter, and that each test starts with the same conditions and a freshly cleaned up environment.


✏ Second Test (2-b): Finding Records by ID in the Database

Now I'll add a another test to find_test.js, but this time I'll check if I can find a user by their ObjectId (the unique ID that Mongoose automatically gives a record when saving).

Looking at the above code, the moment when this ID is generated is when the .save method is called. To make this ID accessible later in an it block below, I'll have to drag the newScore instance of the Score Model out of its current scope.

Also, note that this ObjectId (as the name suggests) is an object, not a string. So I can't simply compare two ObjectIds by triple-equalling them in the assert method, but they need to be converted to strings first:

const assert = require('assert');
const Score = require('../models/scoremodel');

describe('finding records from the database', function () {

    // making this accessible for the second it block below
    let newScore;

    // adding a score record to the empty collection
    beforeEach(function (done) {
        newScore = new Score({
            name: 'jsdisco',
            highscore: 9001
        });

        newScore.save().then(function () {
            done();
        });
    });


    // test 2-a starts here
    it('finds one record from the scores collection', function (done) {
        Score.findOne({ name: 'jsdisco' }).then(function (result) {
            assert(result.name === 'jsdisco');
            done();
        });
    });

    // test 2-b starts here
    it('finds one record by ID from the scores collection', function (done) {
        Score.findById(newScore._id).then(function (result) {
            assert(result._id.toString() === newScore._id.toString());
            done();
        });
    });
});

Running the tests again, and everything works perfectly fine.

To understand the order in which stuff is happening, I've added a console.log in every before and beforeEach call (those are the ones not indented):

mocha-example-5.jpg


✏ Resources

Build a Unit Testing Suite with Mocha and Mongoose


✏ Recap

I've learned

  • how to use Mocha to test the code for MongoDB/Mongoose

  • how to use Mocha Hooks

  • some basic principles of writing good tests


✏ Thanks for reading!

I do my best to thoroughly research the things I learn, but if you find any errors or have additions, please leave a comment below, or @ me on Twitter. If you liked this post, I invite you to subscribe to my newsletter. Until next time 👋


✏ Previous Posts

You can find an overview of all previous posts with tags and tag search here:

#100DaysOfMERN - The App