Testing expressjs
controllers
There is a striking similarity between testing expressjs
route handlers and controllers. That similarity and test exploration is the subject matter of this article.
Few resources about testing in general address advanced concepts such as how to isolate components for better composability and healthy test coverage. One of the components that improve composability, at least in layered nodejs
applications, is the controller.
In this article we will talk about:
- Mocking controller Request/Response objects
- Providing healthy test coverage to controllers
- Avoiding controller integration test trap
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
//Session Object in settings/controller/get-profile
module.exports = function getPrifile(req, res, next){
let user = req.session.user;
UserModel.findById(user._id, (error, user) => {
if(error) return next(error, null);
return req.status(200).json(user);
});
};
This code is a valid controller and a valid handler. There is a caveat in design that makes the case of introducing a service layer in the applications.
What can possibly go wrong?
When trying to figure out how to approach testing expressjs
controllers in a Unit Test context, the following points may be a challenge:
- How to refactor unit test at the time controller layer gets introduced, instead of route handlers.
- Mock database read/write operations, or service layer if any, that are not core/critical to validation of the controller's expectations
- Test-driven refactoring of the controller to adopt a service layer, to abstract the database and third-party services.
The following sections will explore more on making points stated above work.
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 (that made sense to complete current article) on testing expressjs
controllers:
- We can choose amongst a myriad of test runners, for instance,
jasmine
(jasmine-node
),ava
orjest
. We chosemocha
. - The stack
mocha
,chai
andsinon
(assertion and test doubles libraries) worth a shot. supertest
framework for mocking Restful APIs andnock
for mocking HTTP.- 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
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
Show me the tests
If you haven't already, read the “How to write test cases developers will love”
It is not always obvious why to have a controller layer in a nodejs
application. When the controller is already part of the application, it may well be problematic to test it, in a way that provides value to the application as a whole, without sacrificing “time to market”.
describe('getPrifile', () => {
let req, res, next, error;
beforeEach(() => {
next = sinon.spy();
sessionObject = { ... };//mocking session object
req = { params: {id: 1234}, user: sessionObject };
res = { status: (code) => { json: sinon.spy() }}
});
it('returns a profile', () => {
getRequest(req, res, next);
expect(res.status().json).toHaveBeenCalled();
});
it('fails when no profile is found', () => {
getRequest(req, res, next);
expect(next).toHaveBeenCalledWith([error, null]);
});
});
The integration testing of the request may look a bit like in the following paragraph:
var router = require('./profile/router'),
request = require('./support/http');
describe('/profile/:id', () => {
it('returns a profile', done => {
request(router)
.get('/profile/12')
.expect(200, done);
});
it('fails when no profile is found', done => {
request(router)
.get('/profile/NONEXISTENT')
.expect(500, done);
});
});
request = require('./support/http')
is the utility that may use either ofsupertest
ordupertest
provide a request.
Once the above process is refined, more complex use cases can be sliced into more manageable but testable cases. The following as some of the complex use cases we can think of for now:
module.exports = function(req, res, next){
User.findById(req.user, function(error, next){
if(error) return next(error);
new Messenger(options).send().then(function(response){
redisClient.publish(Messenger.SYSTEM_EVENT, payload));
//schedule a delayed job
return res.status(200).json({message: 'Some Message'});
});
});
};
It may be hard to mock one single use case, with callbacks. That is where slicing, and grouping libraries into reusable services can come in handy. Once a library has a corresponding wrapper service, it becomes easy to mock the service as we wish.
module.exports = function(req, res, next){
UserService.findById(req.user)
.then(new Messenger(options).send())
.then(new RedisService(redisClient).publish(Messenger.SYSTEM_EVENT, payload))
.then(function(response){ return res.status(200).json(message);})
.catch(function(error){return next(error);});
};
Alternatively, Using an in-memory database can alleviate the task, to mock the whole database. The other more viable way to go is to restructure the application and add a service layer. The service layer makes it possible to test all these features in isolation.
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 controllers, just like testing routers, can be challenging especially when interacting with external systems is involved. There are additional complimentary materials in the “Testing nodejs
applications” book.
References
- Testing
nodejs
Applications book - How to test
expressjs
controllers. This article covers Mocking Responses, etc. - Mocking Request Promise – with Mockery
- Passing data between Promise callbacks
- Combine data of two async requests to answer both requests
- Bluebird has a .join() function ~ works better than
Promise.all()
- Nock a primer on David Walsh Blog
- Using Nock ~ This approach works more than the way I test WebHooks with pre-programmed responses.
- Unit Testing Express/
mongoose
App routes without hitting the database - Unit Testing Controllers the Easy Way in Express 4 ~ Design Super Build Blog