request / request-promise

The simplified HTTP request client 'request' with Promise support. Powered by Bluebird.

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Possible to stub with sinon.js?

florianschmidt1994 opened this issue · comments

Hello,

I'm trying to stub request-promise using sinon, however could not find a way yet on how to do that.
Back when i was using request itself, it looked something like this:

before(function (done) {
        sinon.stub(request, 'get').yields(null, { statusCode: 200 }, fs.readFileSync(__dirname + '/data/' + filename));
        done();
    });

    after(function (done) {
        request.get.restore();
        done();
    });
}

Any suggestions on how to do this with promise-request?
Feel free to close this issue if it doesn't fit in here.

Hi @florianschmidt1994 , I assume you need a way to create Sinon stubs that return a promise. There are a few Sinon add-ons out there you might try out. I didn't use them myself though.

When you don't want to use an add-on but Sinon in its original form you may "return a promise":

var request = require('request-promise');
var BPromise = require('bluebird');

sinon.stub(request, 'get').returns(BPromise.resolve(fs.readFileSync(__dirname + '/data/' + filename)));

request.get()
    .then(function (responseFromFile) {
        // ...
    });

(Found this neat trick here.)

I hope this helps. It would be great if you let me know what you end up using.

I'll look into into these two packages and post an update when the problem is solved. Thanks!

I solved this issue by mocking the request-promise function and using Bluebird to create the fake response promise.

var Bluebird = require('bluebird');
var sinon = require('sinon');
// ...
it('should mock up a request-promise POST call', function() {
    var rpStub = sinon.stub(rp, 'post'); // mock rp.post() calls

    // calls to rp.post() return a promise that will resolve to an object
    rpStub.returns(Bluebird.resolve({
        fakedResponse: 'will return exactly this'
    }));
});

I am unable to figure out how to mock the entire request-promise module, however. :(
It would be awesome if someone could figure it out - I think the only way to make it happen would be to stub out the actual required dependency, but I would rather not do that.

We found a solution for our problem by mocking the whole request-promise module using mockery

before(function (done) {

    var filename = "fileForResponse"
    mockery.enable({
        warnOnReplace: false,
        warnOnUnregistered: false,
        useCleanCache: true
    });

    mockery.registerMock('request-promise', function () {
        var response = fs.readFileSync(__dirname + '/data/' + filename, 'utf8');
        return bluebird.resolve(response.trim());
    });

    done();
});

after(function (done) {
    mockery.disable();
    mockery.deregisterAll();
    done();
});

describe('custom test case', function() {
    //  Test some function/module/... which uses request-promise 
    //  and it will always receive the predefined "fileForResponse" as a data, e.g.
    //  var request = require('request-promise')
    //  request().then(function(data){
    //  ➞ data is what is in fileForResponse
    //  })
});

As far is i am concerned, this issue can be closed now.

I just added a summary of our discussion as a new section in the README. Thanks for your contributions @MarkHerhold and @florianschmidt1994 !

For future reference, if you want to stick to sinon rather than switching to mockery, can use: sinon.stub(rp, 'get') and change your own modules usage from rp({...}) to rp.get({..}

Also applies to rp.post etc.

Definitely prefer the @MarkHerhold and @Typhlosaurus solution. Bringing in mockery just to stub out a few responses feels like massive overkill.

@MarkHerhold @oshalygin @analog-nico could we have a section on how to do this with proxyquire? Example code would be nice! This really is a pain for me.

I may be late to the party but it is also worth pointing out that you do not need to change your modules as @Typhlosaurus helpfully pointed out.

You can just stub purely using Sinon as follows:

let getStub = sandbox.stub(rp, 'Request');
getStub.resolves(res);

That way you can just keep your http request calls in your modules as follows:

rq(options)
.then((res) => {
     .....
})

The above approach works perfectly fine for me.

I'm not sure if rq.Request has always been exposed from the rp module but I'd bargain it's always been there.

@analog-nico I looked at the R-P docs and when I first used R-P I had the misconception that the only way to mock was to use bulky dependencies. It may be worth noting this approach for people starting fresh dev projects like me that they can stub and mock properly before having to install bulk. Just a helpful suggestion.

FYI in case anyone else has this issue, I managed to mock HTTP requests with Nock (https://github.com/node-nock/nock) instead of stubbing rp. This is not ideal because my unit tests will depend on the internal mechanics of rp, but at least I am able to unit test locally. I wasn't able to get @HystericBerry's solution (or any other solution from #55 or #61) to work - rp kept trying to send out requests despite all my stub attempts with different permutations of mockery and sinon. Glad I tried Nock before digging deeper into why Sinon isn't able to stub Request-Promise.

I would love if someone could explain why Sinon can't stub Request-Promise! The Request-Promise README (https://github.com/request/request-promise#mocking-request-promise) says we need "a library that ties into the module loader and makes sure that your mock is returned whenever the tested code is calling require('request-promise')", which apparently Sinon does not do. Why does Sinon work for stubbing other modules but not RP?

Also learned: In addition to stubbing HTTP requests with Nock, we can also spy on HTTP requests with the isDone() method on Nock expectations: https://github.com/node-nock/nock#isdone

@kaiyuanneo when you say

my unit tests will depend on the internal mechanics of rp

Ok, but what exactly are you doing? Do you know this for sure and can you give examples? And by "internals" what exactly do you mean? It doesn't help RP to say something doesn't work without giving exact details. Are there any code snippets that help us replicate? If what you say is true, then I'm interested in how you are using RP. How are you using RP to rely on the "internals" of RP?

I would love if someone could explain why Sinon can't stub Request-Promise!

It can. Scroll down to 4.

The Request-Promise README (https://github.com/request/request-promise#mocking-request-promise) says we need "a library that ties into the module loader [...]

Not necessarily. That's a solution to a very specific problem. The lack of mock and stub documentation in RP is not an indication that it cannot be done well. Unfortunately, whoever wrote that section of the document made it sound definitive - which is out of date. As a matter of fact, that very example literally came from this thread; therefore, it is not the core RP devs themselves giving a definitive answer that RP is unstub/unmock/able. Rather, that documented code snippet should be seen as a suggestion. Scroll up to see @florianschmidt1994 who suggested that exact solution you referenced.

I probably should have given a more in depth answer in my first comment. The reason why this simplistic approach works is because the require function caches the module. This means that Sinon is actually stubbing the cached RP module in your test and that your tests are stepping through your code using that same cached module. If you don't clear your "require" cache or don't require in a local scope, then the basic, clean implementation works. (Basically, don't do anything funky).

Summarized code below:

// inside foo.js
const rp = require('request-promise-native'); // rely on *require* to cache your module

const options = { }; // setup your options
rp(options)
.then((res) => {
    // no-op
}, console.error);
// inside fooTest.js
const rp = require('request-promise-native'); // rely on *require* to cache your module
const sinon = require('sinon');

describe('...', function() {
    // setup
    it('...', function() {
        sinon.stub(rp, 'Request').resolves({}); 
        // ^ you are actually stubbing the cached Object that "require" loads.
    });
    // teardown
});

Both foo.js and fooTest.js refer to the same Object that require loads.

For anyone trying to make this work with jest.
Here is how you can do it:-

const requestPromise  = require('request-promise');
const mockedRequestPromiseGetCall = jest.spyOn(requestPromise, 'get');
mockedRequestPromiseGetCall.mockImplementation(async () => {
    return {
        fakedResponse: 'will return exactly this'
    }
});

Also do remember to call your api not as

requestPromise(options);

Instead use it in the following way:-

requestPromise.get(options);

The above is needed for the mock to work.

I think @vivek12345 's solution should be added to README as well. it's really helpful.

commented

if you need to assert that the payload is correct for whatever reason and need to use proxyquire so we don't access the cached module like @paulckim mentioned above, this is what worked with sinon.

// fooFile.js
const rp = require('request-promise');

// code to instantiate 
class fooFile {
  constructor { ... };

  async bar(task) {
    await rp.post({
      // payload that needs to be tested
    });
    return;
  }
}
const sinon = require('sinon');
const proxyquire = require('proxyquire');

   it('does a thing', async () => {
      const task = { ... };
      const expectedMessage = { ... };
      const requestStub = sinon.stub().resolves({});
      const fooFile = proxyquire('fooFile', { 'request-promise': { post : requestStub }});
      const fooFile = new fooFile();

      const emptyResponse = await fooFile.bar(task);
      assert.deepEqual(emptyResponse, undefined);
      sinon.assert.calledWith(requestStub, expectedMessage);
   });