How to Stub mongoose
methods and Mock Document Objects
How to Mock mongodb
database access functions
From the cost perspective, the least database read/write operations the better. Not all test cases are created equal, and with intensive read/write capabilities comes big accountability. This blog is an expose of some techniques to mock database access without compromising the quality of test results.
In this article we will talk about:
- Stubbing database access methods to provide mock of their output
- Mocking output of database access chained method
- Mocking
mongoose
/mongodb
connections. - Database Drop in replacement for faster testing
- How to avoid spin-up a database server in a unit test context
- Mocking streaming to/from database systems
Even though this blogpost was designed to offer complementary materials to those who bought my Testing
nodejs
Applications book, the content can help any software developer to tuneup working environment. You use this link to buy the book.
The mongoose model comes with helpers baked in. The example below, implicitly make functions such as save()
, find()
update()
available by default. The trouble starts settling in when we realize that some functions are used with an instance, for example new User().save()
. And others are made available on the class declaration instead, for example: User.find()
etc. From this vantage point, the reality of not applying the same techniques while stubbing starts settling in.
Another real struggle is to be able to figure out how to stub custom helpers. In our example, are statics.findByName()
and methods.addEmail()
. To better understand how to stub those two categories of function, we should start by understanding how they are unique in their own ways, and how they stack up against the instance
and class
functions mentioned above.
Show me the code
We can think of database access from two perspectives. The first is a set of functions designed to extend mongoose
utilities via statics
and methods
properties. The second is from a usage perspective, or other entities using a mongoose model method to talk to the database.
Let's look at both scenarios, first when our functions are expected to extend mongoose
capabilities
var UserSchema = new mongoose.Schema({name: String});
UserScheme.statics.findByName(function(name, next){
//gives: access to Compiled Model
return this.where({'name': name}).exect(next);
});
UserSchema.methods.addEmail(function(email, next){
//works: with retires un-compiled model
return this.model('User').find({ type: this.type }, cb);
});
//exporting the model
module.exports = mongoose.model('User', UserSchema);
Example: mongoose
Model definition example in core/user/model
And next, when our functions are expected to leverage existing capabilities
const Contact = require('core/contact/model');
function addContact(params, next){
return new Contact(params).save((error, contact) => {
if(error) return next(error);
return next(null, contact);
});
}
function findContact(id, next){
return Contact.findById(id, (error, contact) => {
if(error) return next(error);
return next(null, contact);
});
}
function findContacts(params, next){
return Contact.find(params, (error, contacts) => {
if(error) return next(error);
return next(null, contacts);
});
});
Example: mongoose
Model usage example in core/contact/model
What can possibly go wrong?
The following points may be a challenge when testing the model layer:
- Hitting database for any reason slows down Unit Tests
- Stubbing database access functions, while making it possible to validate callback implementations via spies
- Making tests less dependent on database server
- Providing re-usable mocks
Tools
It is feasible to replace database access functions with fakes that emulate similar corresponding actions. Two libraries that come to mind when doing this are sinon
and sinon-mongoose
.
How to apply same techniques to test SQL based alternative to
mongoose
such asknex
?
There is a feature-complete wrapper of most mongoose
utilities: mockgoose
.
Database drop-in replacements
Replacing a database with a drop-in-replacement for testing purposes makes it possible to avoid a database spin-up server altogether. The following example highlights good practices when testing with a live database(local development).
var mongoose = require('mongoose');
describe('User', function(){
before(function(){
mongoose.connect(process.env.CONNECTION_URL);
});
after(function(){
mongoose.connection.close();
mongoose.disconnect();
});
//Do the tests here.
});
This approach is not convenient, since every time we need to test another model, we will need to spin up a database. read/write
operations are also expensive. mockgoose
and mongodb-memory-server
provide alternatives to mock the whole database infrastructure.
For sake of simplicity, we are going to avoid that, and rely on test doubles instead.
Mocking database access functions
Functions that access or change database state, can be replaced by fakes/stubs capable of supply|emulate output identical to data coming from a real database system.
There are a couple of solutions that can be used, one of them is based on test double libraries such as sinon
.
//cb will be the callback to simulate callback function ~ it takes the control from where the stub left off. That means that after executing a stub, the regular callback will execute just as in regular circumstances
function cb(fn, params){
return fn.apply(this, arguments);
//check if params are the one that has to apply instead and apply it.
}
//Model should be an actual model, eg: User|Contact|Address, etc
saveStub = sinon.stub(Contact.prototype, 'save', cb);
findStub = sinon.stub(Contact, 'find', cb);
findByIdStub = sinon.stub(Contact, 'findById', cb);
Stub mongoose
with sinon-mongoose
The following is the order in which libraries are loaded to stub the entire mongoose
library.
First, we will need to replace the default promise with Promise A+
, or another promise library of your choice. Second, we will need to replace mongoose
with sinon-mongoose
. And the trick is completed.
// Using sinon-as-promised with custom promise
var sinon = require('sinon'),
Promise = require('promise'),
require('sinon-as-promised')(Promise);
// Adding sinon-mongoose to mongoose
var mongoose = require('mongoose'),
require('sinon-mongoose');
The promise is not the only kid on the block, in the promised land. The mongoose documentation showcase how to replace the default library, with BYOL (bring your own library).
In next example, we to replace default mongoose
promise library with bluebird
. Another promise library that made rounds in nodejs
and JavaScript
community.
var bluebird = require('bluebird'),
mongoose = require('mongoose'),
mongoose.Promise = bluebird,
uri = 'mongodb://localhost:27017/mongoose_test',
options = { promiseLibrary: bluebird },
db = mongoose.createConnection(uri, options);
That is good as far as information goes, but not necessarily helping our current task at hand.
Mocking Library: — with callbacks
//in model/user.js
var UserSchema = new mongoose.Schema({name: String});
mongoose.model('User', UserSchema);
//in test.spec.js
describe('User', function(){
before(function(){
//model is declared in model/user.js
this.User = mongoose.model('user');
this.UserMock = sinon.mock(this.User);
});
after(function(){
this.User.restore();
});
it('#save()', function(){
var self = this,
user = {name: 'Max Zuckerberg'},
results = Object.assign({}, user, _id: '11122233aabb');
//yields works for callbacks
//.chain('sort').withArgs('-date')
this.UserMock
.expects('save')
.withArgs(user)
.yields(null, results);
new this.User(user).save(function(err, user){
//add all assertions here.
self.UserMock.verify();//verifying
self.UserMock.restore();//restore
});
});
});
Mock works on an actual object, i.e instance of a model. save()
is defined on Document, and not the model object itself. This explains why to we spy on the prototype: sinon.stub(User.prototype, 'save', cb)
.
Without the mock, it becomes impossible to chain extra function such as .exec()
, .stream()
etc. A double stub may be an answer to be able to test such edge cases. A quick example is provided to give an idea what we mean by double stub.
var results = { ... }//mock of user data
sinon
.stub(User.prototype, 'save', cb)
.returns({
exec: sinon.stub().yields(null, results)
});
Alternatively we can use .create() instead. But that may be a little too late, in case the application adopted save()
and used in multiple instances. Needless to mention that .create()
looks a little bit off. Same this in this this instance, but requires far more changes.
It is also possible to rely on libraries such as factory girl, as explained in this SO answer
Mocking Library: — with promises
//in User.js
MongooseModel
.find()
.limit(10)
.sort('-date')
.exec()
.then(result => result);
//in user.model.spec.js
require('sinon'),
require('mongoose'),
require('sinon-mongoose'),
require('sinon-as-promised');
//describe
describe('User', function(){
it('works', function(){
sinon.mock(MongooseModel)
.expects('find').withArgs(10)
.chain('limit').withArgs(10)
.chain('sort').withArgs('-date')
.chain('exec')
.resolves('SOME_VALUE');
//.yields(null, 'SOME_VALUES')
});
});
Mocking Library: — with streams
The following is a drop-in-replacement of usage of a model paired with a stream.
//code example
UserModelMock
.find()
.stream()
.pipe(new Transformer())
.pipe(res);
// in tests ~ using the double mock technique ~ return a readableStream
sinon.stub(Model, 'find').returns({
stream: sinon.stub().yields(null , readableStreamMock)
});
//readableStreamMock has to have generated data for testing purposes.
//in tests ~ create writable stream compatible with response object somehow
writableStream.on('data|end|close|finish', function(){
expect(Model.find.called, 'find() has been called');
});
The technique to test the streams has been intensively covered in the – Testing
nodejs
Applications book. The ideas in this code sample, are rough and still have loopholes that are covered in the said book.
Final notes
Models should be created once, across all tests.
The error: OverwriteModelError: Cannot overwrite 'Activity' model once compiled.
means one of the following occurred:
– 1. got the caps wrong while importing models. => import User from 'model/user
– 2. got wrong definitions of models => var userSchema = new Schema({}); module.exports = mongoose.model('user', userSchema)
<=== new schema and not just schema(this was my case)
– 3. got models twice(two times recompilation) => module.exports = mongoose.model.User || mongoose.model('user', userSchema);
There are more answers on this subject on StackOverflow
Conclusion
In this article, we established the cost of hitting the database every time a unit test runs, and how to avoid worst-case scenario by mocking out the most expensive parts of the model layer.
We also reviewed how to reduce such costs by strategically stubbing read/write operations to make tests fast, without losing test effectiveness.
Testing tends to be more of art, than a science, practice makes perfect. There are additional complimentary materials in the “Testing nodejs
applications” book.
References
- Testing
nodejs
Applications book - A TDD Approach to Building a Todo API Using
nodejs
andmongodb
~ SemaphoreCI Community Tutorials - Mocking Model Level ~ SemaphoreCI Community Tutorials
- Mocking
mongoose
with Promises ~ underscopeio Source Code - 40 database platforms we are targeting for streaming ~ Api Friends Blog
- SQL database mocking utility ~
mock-knex
knex
bookshelf mocks and unit tests ~@jomaora
Medium