简体   繁体   中英

How to unit test a function which calls another that returns a promise?

I have a node.js app using express 4 and this is my controller:

var service = require('./category.service');

module.exports = {
  findAll: (request, response) => {
    service.findAll().then((categories) => {
      response.status(200).send(categories);
    }, (error) => {
      response.status(error.statusCode || 500).json(error);
    });
  }
};

It calls my service which returns a promise. Everything works but I am having trouble when trying to unit test it.

Basically, I would like to make sure that based on what my service returns, I flush the response with the right status code and body.

So with mocha and sinon it looks something like:

it('Should call service to find all the categories', (done) => {
    // Arrange
    var expectedCategories = ['foo', 'bar'];

    var findAllStub = sandbox.stub(service, 'findAll');
    findAllStub.resolves(expectedCategories);

    var response = {
       status: () => { return response; },
       send: () => {}
    };
    sandbox.spy(response, 'status');
    sandbox.spy(response, 'send');

    // Act
    controller.findAll({}, response);

    // Assert
    expect(findAllStub.called).to.be.ok;
    expect(findAllStub.callCount).to.equal(1);
    expect(response.status).to.be.calledWith(200); // not working
    expect(response.send).to.be.called; // not working
    done();
});

I have tested my similar scenarios when the function I am testing returns itself a promise since I can hook my assertions in the then.

I also have tried to wrap controller.findAll with a Promise and resolve it from the response.send but it didn't work neither.

You should move your assert section into the res.send method to make sure all async tasks are done before the assertions:

var response = {
   status: () => { return response; },
   send: () => {
     try {
       // Assert
       expect(findAllStub.called).to.be.ok;
       expect(findAllStub.callCount).to.equal(1);
       expect(response.status).to.be.calledWith(200); // not working
       // expect(response.send).to.be.called; // not needed anymore
       done();
     } catch (err) {
       done(err);
     }
   },
};

The idea here is to have the promise which service.findAll() returns accessible inside the test's code without calling the service . As far as I can see sinon-as-promised which you probably use does not allow to do so. So I just used a native Promise (hope your node version is not too old for it).

const aPromise = Promise.resolve(expectedCategories); 
var findAllStub = sandbox.stub(service, 'findAll');
findAllStub.returns(aPromise);

// response = { .... }

controller.findAll({}, response);

aPromise.then(() => {
    expect(response.status).to.be.calledWith(200);
    expect(response.send).to.be.called;    
});

When code is difficult to test it can indicate that there could be different design possibilities to explore, which promote easy testing. What jumps out is that service is enclosed in your module, and the dependency is not exposed at all. I feel like the goal shouldn't be to find a way to test your code AS IS but to find an optimal design.

IMO The goal is to find a way to expose service so that your test can provide a stubbed implementation, so that the logic of findAll can be tested in isolation, synchronously.

One way to do this is to use a library like mockery or rewire . Both are fairly easy to use, (in my experience mockery starts to degrade and become very difficult to maintain as your test suite and number of modules grow) They would allow you to patch the var service = require('./category.service'); by providing your own service object with its own findAll defined.

Another way is to rearchitect your code to expose the service to the caller, in some way. This would allow your caller (the unit test) to provide its own service stub.

One easy way to do this would be to export a function contstructor instead of an object.

module.exports = (userService) => {

  // default to the required service
  this.service = userService || service;

  this.findAll = (request, response) => {
    this.service.findAll().then((categories) => {
      response.status(200).send(categories);
    }, (error) => {
      response.status(error.statusCode || 500).json(error);
    });
  }
};

var ServiceConstructor = require('yourmodule');
var service = new ServiceConstructor();

Now the test can create a stub for service and provide it to the ServiceConstructor to exercise the findAll method. Removing the need for an asynchronous test altogether.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM