Testing failed requests in node

2019-07-16 14:20发布

问题:

I have some code that looks like the following:

var request = require('request');

function Service(){
 this._config = require('../path/to/config.json');
}

Service.prototype.doThing = function(){
  return new Promise(function(resolve, reject){
    request.post(url, {payload}, function(error, response, body){
      //handle response
      resolve(true);
    }).on('error', function(err){
      //handle errors
      resolve(false);
    });
  });
}

I'm trying to test the block that runs on error, but having difficulty because of the promise. I'm using mocha for test running and sinon for stubbing. I'm able to stub request so that I can count the number of times the on method is called, but the containing Promise never resolves.

There are some packages that work with sinon to handle promises (I've tried sinon-as-promised and sinon-stub-promise), but I have to stub the entire doThing method in order for it to resolve properly. I would appreciate any input on the propper way to test this code or alternate code structures that may be easier to test.

The test in question (that hangs waiting for the doThing promise to return) is below:

context('when the server is unavailable', function(){
  beforeEach(function() {
    var onStub = sinon.stub();
    requestStub = {post: function(){ return {on: onStub}}};
    Service = proxyquire('path/to/service', {request: requestStub});
    service = new Service();
  });

  it('should not set the auth key', function(){
    return service.doThing().then(function(x){
      return assert(onStub.calledOnce);
    });
  });
});

Thanks!

回答1:

To sum up the obvious, your problem here is that you do not control the request object, or rather the response from its post method. If you can control that you will be able to test all the different code paths in your doThing method.

You have three possibilities for controlling the response from request:

  1. Stub out the network layer (using something like Nock) so you can control what the HTTP layer is handing over to the request library.
  2. Use dependency injection to inject a stub that can be used in place of request.post (will require a code modification)
  3. Use a link seam to that will substitute the object returned from require() calls with a stub object you control. This is of course what you have done.

Personally, I would have gone for option 2 since that requires no extra framework, is easy to reason about and is easy to implement. You can refer to this quite elaborate example, but the code needed in your case is minimal.

Anyway, since you have chosen option 3 I might as well go down that route :-) The problem is how you stubbed out the post method. Have will that ever call your callback?

I would change the post stub to something like this:

post: (url, options, cb) => { 
  cb(null, null, null); // will resolve your promise in the module
  return { on: onStub }; 
}

Of course, the nulls passed into your callback can be something else should you need to do something with the values later on.