Search code examples
node.jsunit-testingmocha.jssinonsinon-chai

Stub not getting called inside .then()


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.


Solution

  • 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.