This is a quick Node.js tutorial on integration testing asynchronously created mongoose models for Mongodb using mocha. TLDR: add the following in your after
method:
mongoose.models = {};
mongoose.modelSchemas = {};
The setup
The primary assumption here is that we have certain requirements which force us to instantiate our mongoose models asynchronously (a plugin requiring data loaded from the db for example). In true TDD fashion, we will first setup the tests for our tutorial:
'use strict';
const expect = require('chai').expect;
const modelContainer = require('../lib/model-container');
describe('User model data', () => {
let User = null;
before(() => {
return modelContainer.init()
.then(models => {
User = models.User;
});
});
it('should add and find a user by username', () => {
let userData = {username: 'abc'};
let user = new User(userData);
// when
return user.save()
.then(savedUser => {
return User.findOne({ username: userData.username });
})
.then(found => {
// then
expect(found.username).to.equal(userData.username);
});
});
});
In the above test, we notice that we are using a modelContainer
which has an init
method which is responsible for asynchronously initializing the mongodb connection and then defining the mongoose models. This model-container.js
file can look something like the following:
'use strict';
const mongoose = require('mongoose');
const Promise = require('bluebird');
mongoose.Promise = Promise;
let modelContainer = {};
modelContainer.init = function() {
return mongoose.connect('mongodb://...')
.then(() => {
return doSomeMoreAsyncStuff();
})
.then(someResult => {
const UserSchema = require('./schemas/user')(someResult);
return {
User: ic.mongoose.model('User', UserSchema)
};
});
};
module.exports = modelContainer;
The User model itself is now defined as follows:
'use strict';
const mongoose = require('mongoose');
const somePlugin = require('somePlugin');
module.exports = function(someResult) {
const UserSchema = new mongoose.Schema({
username: { type: String, unique: true, required: true },
});
UserSchema.plugin(somePlugin, { result: someResult });
return UserSchema;
};
The problem
The initial test written above now passes without any issues. We will now come to the heart of the problem which is that if we were to add another test file that initializes the mongoose models and run all the tests then the fact that the models are being intitialized multiple times leads to the following cryptic error: OverwriteModelError: Cannot overwrite `User` model once compiled
.
Here's an example of another test:
'use strict';
const expect = require('chai').expect;
const modelContainer = require('../lib/model-container');
describe('User model validation', () => {
let User = null;
before(() => {
return modelContainer.init()
.then(models => {
User = models.User;
});
});
it('should fail when adding a user with the same username', () => {
let userData = {username: 'abc'};
let user1 = new User(userData);
let user2 = new User(userData);
// when
return user1.save()
.then(savedUser1 => {
return user2.save();
})
.then(savedUser2 => {
expect.fail();
})
.catch(err => {
// then
expect(err.name).to.equal('MongoError');
expect(err.code).to.equal(11000);
});
});
});
The solution
One possible solution is to delete the models defined by mongoose after the test suite has run:
after(() => {
mongoose.models = {};
mongoose.modelSchemas = {};
return mongoose.connection.close();
});
The will need to be added in every test file and the requisite mongoose wrapper will need to be exposed (possibly as a return value from the modelContainer
).
Other workarounds could include trying to initialize the models and catching the OverwriteModelError
and returning the models via the mongoose.model()
call but it is preferable to not litter production code with testing conditions.
Feel free to get in touch if you have any other solutions, happy testing!