I am working on unit testing an Express application. I am trying to mock up my external dependencies (Express, database, etc) and am close to a break through.
However, I am having issues with stubs not being called from within a .then()
inside my business logic.
A couple of the methods I am attempting to test are the following:
/**
* Ping
* A simple endpoint to poke for testing.
* @arg {*} request - Incoming request
* @arg {*} response - Outgoing response
* @arg {*} next - Next route in series
*/
ping: (request, response, next) => {
let elapsed = Date.now() - request.start;
response.status(200).json({
request_started: new Date(request.start).toISOString(),
request_duration: `${elapsed} milliseconds`
});
},
/**
* Registration
* Allows for registration of a user name and password.
* @arg {*} request - Incoming request
* @arg {*} response - Outgoing response
* @arg {*} next - Next route in series
*/
register: (request, response, next) => {
let username = request.body.username;
let password = request.body.password;
this.model.registerUser(username, password).then(ret => {
if (ret) {
response.status(201).send("Created");
} else {
response.status(400).send("Error processing registration request. Please try again.");
}
}).catch(err => {
response.status(400).send("Error processing registration request. Please try again.");
});
}
The model in register
returns a Promise
that wraps a call to a database and replies based on the outcome. I have a mock of this setup as follows:
mockModel: {
registerUser: sinon.stub().callsFake(function(user, pass) {
if (typeof user !== 'undefined') {
return Promise.resolve(pass === 'pass');
}
}),
getUser: sinon.stub().callsFake(function(user, pass) {
if (typeof user !== 'undefined' && pass === 'pass') {
return Promise.resolve({id: 9999});
} else {
return Promise.resolve(false);
}
})
}
I also have the response
object mocked so I can pass it in and determine if it is called correctly:
mockRes: () => {
return {
status: sinon.stub().returnsThis(),
send: sinon.stub(),
json: sinon.stub()
};
}
The problem arises when I get to the tests:
describe('Register() method', function() {
this.beforeEach(function() {
req = mockReq(0, {}, {username: 'user', password: 'pass'});
res = mockRes();
base.BaseRoutes(router, null, base.methods, mockModel);
});
it('Returns a 201 Created message upon success', function() {
base.methods.register(req, res);
chai.expect(res.status).to.have.been.calledWith(201);
chai.expect(res.send).to.have.been.calledWith('Created');
});
});
The test here fails with the following error:
1) Base Methods
Register() method
Returns a 201 Created message upon success:
AssertionError: expected stub to have been called with arguments 201
at Context.<anonymous> (test\spec.js:50:50)
Stepping through with a debugger shows that the method is getting called, yet I'm getting this failure.
Other tests in the same suite that leverage the same mocked request/response work correctly but they don't use Promise
calls (example: ping() method)
I suspect that it has something to so with scoping, but I'm not sure where things are going wrong.
After running through this a few more times, I found that it was not a scope issue, but an asynchronous issue. The test case was completing before the Promise
resolved/rejected.
The piece that I was missing to close the loop was that my Express route handlers needed to return the Promise
that they created to handle the database call:
register: (request, response, next) => {
let username = request.body.username;
let password = request.body.password;
return this.model.registerUser(username, password).then((ret) => {
if (ret) {
response.status(201).send("Created");
} else {
response.status(400).send("Error processing registration request. Please try again.");
}
}, (err) => {
response.status(400).send("Error processing registration request. Please try again.");
});
},
And then the test, perform my assertions in the then()
on the returned Promise
:
it('Returns a 201 Created message upon success', function(done) {
base.methods.register(req, res).then(x => {
chai.expect(res.status).to.have.been.calledWith(201);
chai.expect(res.send).to.have.been.calledWith('Created');
}).then(done, done);
});
it('Returns a 400 message upon failure', function(done) {
req = mockReq(0, {}, {username: 'user', password: 'fail'});
base.methods.register(req, res).then(x => {
chai.expect(res.status).to.have.been.calledWith(400);
chai.expect(res.send).to.have.been.calledWith(sinon.match.string);
}).then(done, done);
While there were examples of passing the Promise
to the test and handling it there and examples on testing Express route handlers in isolation, I couldn't find examples that combined the two and tested it. Hopefully this can be of service to someone else.