Testing nodejs
models
Testing the model layer introduces a set of challenges relating to reading and writing to a database. This article clears some of the challenges to avoid side effects and makes it possible to test the model layer in isolation.
One of the components that lay the groundwork for data-driven layered applications is the model layer. However, resources about testing, in general, do not address advanced concepts such as how to isolate components for better composability and healthy test coverage.
In this article we will talk about:
- Basics when testing models
- Best practices around model layer unit testing.
- Mocking read/write and third party services to avoid side effects.
Even though this blog post 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.
Show me the code
//in lib/models/user.js
var UserSchema = new mongoose.Schema({name: String});
UserScheme.statics.findByName(function(name, next){
//gives: access to Compiled Model
return this.where({'name': name}).exec(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);
//anywhere else in UserModel is used
User.findById(id, function(error, user){
if(error) return next(error);
return res.status(200).json(user);
});
new User(options).save(function(error, user){
if(error) return next(error);
return next(null, user);
});
Example: mongoose
Model definition example in model/user.js
What can possibly go wrong?
When trying to figure out how to approach mocking chained model read/write functions, the following points may be a challenge:
- Stub database read/write operations ~ finding a balance between what we want to test, versus what we want to mock
- Mock database read/write operation outputs ~ output may not reflect reality after schema(table definition) change.
- Cover exceptions and missing data structures ~ databases are complex systems, and we may not cover the majority of scenarios where errors/exceptions may occur
- Avoid integration testing traps ~ the complexity of database systems makes it hard to stick to the plan and write tests that validate our actual implementation
Choosing tools
If you haven't already, reading “How to choose the right tools” blog post gives insights on a framework we used to choose the tools we suggest in this blog.
Following our own “Choosing the right tools” framework, we adopted the following tools, when testing mongoose
models:
- We can choose amongst a myriad of test runners, for instance,
jasmine
(jasmine-node
),ava
orjest
. We recommendmocha
. The stackmocha
,chai
andsinon
can be worth it as well. - Code under test is instrumented, but default reporting tools do not always suits our every project's needs. For test coverage reporting we recommend
istanbul
.
Workflow
It is possible to generate reports as tests progress.
latest versions of
istanbul
usesnyc
code name.
# In package.json at "test" - add next line
> "istanbul test mocha -- --color --reporter mocha-lcov-reporter specs"
# Then run the tests using
$ npm test --coverage
Example:
Show me the tests
If you haven't already, read the “How to write test cases developers will love”
This blog post approaches testing of fairly large nodejs
application from a real-world perspective and with refactoring in mind.
sinon
stubs to simulate a response from Mongo::UserSchema::save()
function, its equivalents.
describe('User', () => {
beforeEach(() => {
ModelSaveStub = sinon.stub(User.prototype, 'save', cb);
ModelFindStub = sinon.stub(User, 'find', cb);
ModelFindByIdStub = sinon.stub(User, 'findById', cb);
});
afterEach(() => {
//...
});
it('should findByName', (done) => {
User.findByName('Jane Doe', (error, users) => {
expect(users[0].name).toBe('Jane Doe');
done();
});
});
it('should addEmail', (done) => {
User.addEmail('jane.doe@jd.com', (error, email) => {
expect(email).toBe('Jane Doe');
done();
});
});
});
To learn more about mocking database functions, please read this article.
Conclusion
Automated testing of any JavaScript project is quite intimidating for newbies and veterans alike. In this article, we reviewed how testing tends to be more of art, than science.
We also stressed the fact that, like in any art, practice makes perfect ~ testing models is challenging especially when a read/write to an actual database is involved. There are additional complimentary materials in the “Testing nodejs
applications” book.
References
- Testing
nodejs
Applications book - Mocking database calls by wrapping
mongoose
withmockgoose
- Getting started with
nodejs
and Mocha - StackOverflow response that works for stubbing