Search code examples
javascriptpromisesinonstub

Cannot Stub Function Returning Promise


I was trying to stub an arrow function removeUserEntry, but when executing acctRmRouter in the test, my stub seems being ignored. I have to explicitly stub the UserModel's deleteOne method to get the test successfully, I am wondering why the ignorance happens, thank you

acctRoute.js

const removeUserEntry = (username) => {
    const condition = {username: username};

    return UserModel.deleteOne(condition)
       .then((res) => {
           if (res.n < 1) {
               throw new Error('User not exists');
           }
           return true;
       }, (err) => {throw err})
       .catch(err => err);
};

const acctRmRouter = function(httpReq, httpRes, next) {
    if (!argValidate(httpReq.body, 'string')) {
        httpRes.locals = {api: { success: false }};
        // return to avoid running downwards
        return next(new Error('bad argument'));
    }

    // perform DB rm user
    return removeUserEntry(httpReq.body).then((res) => {
        if (res === true) {
            httpRes.locals = {api: { success: true }};
            next();
        } else {
            httpRes.locals = {api: { success: false }}
            next(res);
        }
    });
};

acctRoute.spec.js

it('should remove user handler pass success request', async () => {
    shouldDbReset = false;
    const mockRequestURL = "/api/account/rm-user";
    const mockRequest = httpMocks.createRequest({
        method: "POST",
        url: mockRequestURL,
        headers: {
            "Content-Type": "text/plain"
        },
        body: 'validRmUser',
    });
    const mockResponse = httpMocks.createResponse();
    const spyNext = sinon.spy();
    const stubRemoveUserEntry = sinon.stub(accountRouterHelper, 'removeUserEntry'); 

    stubRemoveUserEntry.callsFake(function(){
        return Promise.resolve(true);
    }); // Expecting this function to be stubbed, and always return true

    await accountRouterHelper.acctRmRouter(mockRequest, mockResponse, spyNext); 
    /* But when running the function, it returns error object with "User not exists" 
    which is not what intended */

    const firstCallArgs = spyNext.getCall(0).args[0];

    expect(spyNext.called).to.be.true;
    console.log(`firstCallArgs: ${firstCallArgs}`)
    expect(firstCallArgs instanceof Error).to.be.false;
    expect(spyNext.args[0].length).to.equal(0);
    expect(mockResponse.statusCode).to.equal(200);
    expect(mockResponse.locals.api.success).to.be.true;

    stubRemoveUserEntry.resetHistory();
    stubRemoveUserEntry.restore();
});

The following indeed stubbed successfully with similar pattern to removeUserEntry.

acctRoute.js

const createUserEntry = (userData) => {
    const updatedUserData = filterInput(userData);
    const userDoc = new UserModel(updatedUserData);
    return userDoc.save()
    .then((userObj) => userObj._doc
    ,(err) => { throw err;})
    .catch(err => err);
};

const acctCreateRouter = function (httpReq, httpRes, next) {
// do something in mongodb
return createUserEntry(userCondition)
   .then((response) => {
            if (!(response instanceof Error)) {
                httpRes.locals = {api: { success: true}};
                next();
            } else {
                httpRes.locals = {api: { success: false}};
                next(response);
            }
        }, (err) => {
            httpRes.locals = {api: { success: false}};
            next(err);
        })
        .catch((err) => {
            httpRes.locals = {api: { success: false}};
            next(err);
        });     
};

const acctOutputRouter = function(req, res, next) {
    if (res.locals) {
        res.send(res.locals.api);
    } else {next()}
};

acctRoute.spec.js

it("should return and save the success result to response locals for next route", () => {
        shouldDbReset = false;
        const mockResponse = httpMocks.createResponse();
        const stubCreateUserEntry = sinon.stub(accountRouterHelper, 'createUserEntry');
        const mockNext = sinon.spy();

        stubCreateUserEntry.callsFake(function(){
            return Promise.resolve();
        }); // Unlike removeUserEntry, stubbing neatly with desired output
        return accountRouterHelper.acctCreateRouter(mockRequest, mockResponse, mockNext)
        .then(() => {
            expect(mockNext.called).to.be.true;
            expect(mockResponse.locals.api.success).to.be.true;
        })
        .finally(() => {
            mockNext.resetHistory();
            stubCreateUserEntry.restore();
        });
    });

Solution

  • Issue

    sinon.stub(accountRouterHelper, 'removeUserEntry') replaces the module export.

    acctRmRouter() is not calling the module export, it is calling removeUserEntry() directly so stubbing the module export does nothing.

    Solution

    Refactor acctRmRouter() to call the module export for removeUserEntry().

    ES6

    // import module into itself
    import * as self from './acctRoute';
    
    ...
    
    const acctRmRouter = function(httpReq, httpRes, next) {
    
        ...
    
        // call the function using the module
        return self.removeUserEntry(httpReq.body).then((res) => {
    
        ...
    

    Node.js module

    ...
    
    const acctRmRouter = function(httpReq, httpRes, next) {
    
      ...
    
      // call the function using module.exports
      return module.exports.removeUserEntry(httpReq.body).then((res) => {
    
      ...