Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

stub#usingPromise and stub#onNthCall have unexpected interactions #1474

Closed
kara-ryli opened this issue Jun 28, 2017 · 13 comments
Closed

stub#usingPromise and stub#onNthCall have unexpected interactions #1474

kara-ryli opened this issue Jun 28, 2017 · 13 comments

Comments

@kara-ryli
Copy link

kara-ryli commented Jun 28, 2017

Using the latest version of just about everything:

    "node": 6.11.0",
    "bluebird": "3.5.0",
    "chai": "4.0.2",
    "chai-as-promised": "7.0.0",
    "mocha": "3.4.2",
    "sinon": "2.3.5",
    "sinon-chai": "2.11.0"

I run into a couple interactions between stub#usingPromise and stub#onNthCall.

My test is testing an internal method call that returns a Bluebird promise. I want to make sure that subsequent calls after a failure work correctly.

Code:

const Bluebird = require('bluebird');
const chai = require('chai');
const chaiAsPromised = require('chai-as-promised');
const sinon = require('sinon');
const sinonChai = require('sinon-chai');

const expect = chai.expect;

chai.use(sinonChai);
chai.use(chaiAsPromised);

describe('repro', () => {
  it('should work', () => {
    var s = sinon.stub();
    function test() {
      return s().tap(() => true);
    }
    s.
      onFirstCall().usingPromise(Bluebird).rejects(new Error('oops')).
      onSecondCall().usingPromise(Bluebird).resolves('yay');
    return expect(test()).to.be.rejected.then(() => {
      return expect(test()).to.be.eventually.eql('yay');
    });
  });

  it('should maybe work', () => {
    var s = sinon.stub();
    function test() {
      return s().tap(() => true);
    }
    s.
      onFirstCall().rejects(new Error('oops')).usingPromise(Bluebird).
      onSecondCall().resolves('yay').usingPromise(Bluebird);
    return expect(test()).to.be.rejected.then(() => {
      return expect(test()).to.be.eventually.eql('yay');
    });
  });

  it('should also work', function () {
    var s = sinon.stub();
    function test() {
      return s().tap(() => true);
    }
    s.usingPromise(Bluebird).
      onFirstCall().rejects(new Error('oops')).
      onSecondCall().resolves('yay');
    return expect(test()).to.be.rejected.then(() => {
      return expect(test()).to.be.eventually.eql('yay');
    });
  });
});

None of these tests pass, failing on TypeError: s(...).tap is not a function. However, I can get them to pass by changing my implementation:

describe('regressions', () => {
  it("works", () => {
    var s = sinon.stub();
    function test() {
      return Bluebird.try(() => s()).tap(() => true);
    }
    s.
      onFirstCall().rejects(new Error('oops')).
      onSecondCall().resolves('yay');
    return expect(test()).to.be.rejected.then(() => {
      return expect(test()).to.be.eventually.eql('yay');
    });
  });

  it('also works', () => {
    var s1 = sinon.stub();
    var s2 = sinon.stub();
    function test1() {
      return s1().tap(() => true);
    }
    function test2() {
      return s2().tap(() => true);
    }
    s1.usingPromise(Bluebird).rejects(new Error('oops'));
    s2.usingPromise(Bluebird).resolves('yay');
    return expect(test1()).to.be.rejected.then(() => {
      return expect(test2()).to.be.eventually.eql('yay');
    });
  });
});

But I'd prefer not to change the implementation to fit the tests.

@mroderick
Copy link
Member

I can't quite wrap my head around this just yet, so I can tell if this is a bug or not.

In the meantime, there's an easier way to use third party promise libraries with Sinon

const Bluebird = require('bluebird');
const sandbox = require('sinon').sandbox.create().usingPromise(Bluebird);
// all stubs created with this sandbox will use Bluebird for promises

Perhaps you can try using that to see if that exposes a flaw in your implementation or makes a bug in Sinon easier to identify?

@fulax
Copy link

fulax commented Jun 29, 2017

Hi there. I'm hitting a similar issue, with a simpler test case:

const bluebird = require('bluebird');
const sinon = require('sinon');

const myStub = sinon.stub().usingPromise(bluebird.Promise);

// works
myStub.resolves(['default array']);
myStub().tap(console.log);

// does not work: TypeError: myStub(...).tap is not a function
myStub.onSecondCall().resolves(['second array']);
myStub().tap(console.log);

I tested it using both sinon 2.3.5 and 2.3.6, same behavior. I'm using bluebird 3.5.0, with nodejs v6.11.0 on Debian Jessie.

Using sinon's sandbox gave me the same unexpected result.

@fatso83
Copy link
Contributor

fatso83 commented Jun 30, 2017

@fulax and @ry7n , do you know if this worked correctly at some earlier point? I'd like to find out if we have a regression, or if this bug was there all along (if a bug - which it looks like).

@kara-ryli
Copy link
Author

@fatso83: I noticed this issue upgrading from sinon@1.x + sinon-as-promised to sinon@2.x, so I never saw such a test work in 2.x. I think a simple example case of the correct ordering is fine, i.e. can you re-write this:

  it('should work', () => {
    var s = sinon.stub();
    function test() {
      return s().tap(() => true);
    }
    s.usingPromise(Bluebird).
      onFirstCall().rejects(new Error('oops')).
      onSecondCall().resolves('yay');
    return expect(test()).to.be.rejected.then(() => {
      return expect(test()).to.be.eventually.eql('yay');
    });

To pass without modifying the test() function? But from a bunch of iterating, I couldn't find a combo of #usingPromise and #onNthCall that worked correctly.

@HugoMuller
Copy link
Contributor

HugoMuller commented Jul 12, 2017

I guess that the internal stub's attribute promiseLibrary, (set with usingPromise) is not propagated to the behaviors generated with onCall(n). More precisely, this.promiseLibrary is undefined.

I tried to run the @fulax's code in debug mode, and set a break point in the body of the invoke method, just here: return (this.promiseLibrary || Promise).resolve(this.returnValue);

You will see that this.promiseLibrary is correctly set for the original stub (this.promiseLibrary === bluebird constructor). But it is undefined for the behavior defined with onSecondCall().

Edit: Adding this couple of lines in proto.create before returning the brand new behavior seems to solve the issue.

if(stub.defaultBehavior && stub.defaultBehavior.promiseLibrary){
  behavior.promiseLibrary = stub.defaultBehavior.promiseLibrary;
}

@fatso83
Copy link
Contributor

fatso83 commented Jul 12, 2017

@HugoMuller good find! any chance of wrapping it into a PR?

@HugoMuller
Copy link
Contributor

Yeah, I'm working on it ;)

@mroderick
Copy link
Member

Fixed by #1484

@brian-mann
Copy link

The exact same problem still exists when using withArgs instead of onCall.

@mroderick
Copy link
Member

The exact same problem still exists when using withArgs instead of onCall.

@HugoMuller since you already understand this part well, would you mind investigating that?

@HugoMuller
Copy link
Contributor

Ok, I'll take a look at this :)

@HugoMuller
Copy link
Contributor

done => #1497

@fatso83
Copy link
Contributor

fatso83 commented Jul 26, 2017

I ❤️ the speed at which things are being fixed and releases are getting out these days. Even though it was a long birth, we must have been doing something right with the work leading up to 2.0 😄

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging a pull request may close this issue.

6 participants