✏ 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:
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:
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:
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:
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):
✏ 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: