Sinon: New stub API failing after replacing calls with .callsFake

Created on 19 Mar 2017  ·  23Comments  ·  Source: sinonjs/sinon

  • Sinon version : 2.0.0
  • Environment : node 7.7.3
  • Other libraries you are using: sinon-test, sinon-chai, chai.

What did you expect to happen?
Migrating sinon.stub syntax from 1.x to 2.0.0 either manually or using sinon-codemod should not alter test output.

What actually happens
All tests using a stubbed object fail with an endless stacktrace (excerpt shown below).

 at Object.proxy [as prefetch] (node_modules/sinon/lib/sinon/spy.js:97:22)
      at Object.invoke (node_modules/sinon/lib/sinon/behavior.js:130:32)
      at Object.functionStub (node_modules/sinon/lib/sinon/stub.js:83:53)
      at Function.invoke (node_modules/sinon/lib/sinon/spy.js:190:51)
      at Object.proxy [as prefetch] (node_modules/sinon/lib/sinon/spy.js:97:22)
      at Object.invoke (node_modules/sinon/lib/sinon/behavior.js:130:32)
      at Object.functionStub (node_modules/sinon/lib/sinon/stub.js:83:53)
      at Function.invoke (node_modules/sinon/lib/sinon/spy.js:190:51)
      at Object.proxy [as prefetch] (node_modules/sinon/lib/sinon/spy.js:97:22)
      at Object.invoke (node_modules/sinon/lib/sinon/behavior.js:130:32)
      // ....

Reverting _back to the old syntax_ prints out a warning, but makes all tests work as expected:

sinon.stub(obj, 'meth', fn) is deprecated and will be removed fromthe public API in a future version of sinon.
 Use stub(obj, 'meth').callsFake(fn).
 Codemod available at https://github.com/hurrymaplelad/sinon-codemod

On a related note, there's a typo in the warning message: "fromthe public" -> "from the public"

Also, calls like these will show the exact same warning - although I'm not really sure what's the new expected syntax there:

this.stub(seneca, 'export')
        .withArgs('transport/utils').returns(transportUtils);

How to reproduce
In my scenario, it was as simple as replacing calls like this:

sinon.stub(channel, 'assertQueue', channel.assertQueue);

With the new 2.0.0 stub API:

sinon.stub(channel, 'assertQueue').callsFake(channel.assertQueue);

You can also try and pull the entire failing test suite from seneca-amqp-transport repo:

git clone -b enhancement/sinon-2.0.0 --single-branch [email protected]:senecajs/seneca-amqp-transport.git
cd seneca-amqp-transport && npm i
npm run validate

Most helpful comment

Took me quite some time to figure out, because for me the simple replacement with .callsFake wasn't working, too. When replacing all reset() calls with resetHistory(), as @emirotin discovered, it actually works again. This should really be part of the deprecation/update guide.

All 23 comments

Thanks for providing a good issue and test case! I will look into it, and it seems we might also need to update/add to the docs on this matter.

@fatso83 You're welcome. Let me know if you need something else.

@fatso83 Just tried with newly released 2.1.0 and the same thing happens, unfortunately. No idea if this was actually tackled (probably not, seen as this issue is still open), but just thought you'd like to know.

One thing that _could_ be problematic is the usage of sinon-chai which has an explicit peerDependency on sinon < 2. Doesn't explain why using the old syntax would actually work, though.

Hi @nfantone, since I know the Chai codebase and a bit about a few of its plugins I'll take a look at how sinon-chai works and I'll let you know my findings.

If you have any other ideas or code examples please let me know.

For now you can just use spies instead of stubbing a method with itself, this is probably causing the infinite recursion you're dealing with. Anyway, your use case should be supported and I really think we should fix it.

How would I go about turning my stubs into spies? Would spies call a fake method as well? Could you give me an example? Say we have something like my original case:

sinon.stub(channel, 'assertQueue', channel.assertQueue);

What should the new syntax look like using spies?

By doing this you are just telling your stub to call the original channel.assertQueue method, which is exactly the same as just using a spy, which allows you to keep track of calls, arguments and other important pieces of information while still calling the old behavior.

You could, for example, turn this:

sinon.stub(channel, 'assertQueue', channel.assertQueue);

Into this:

sinon.spy(channel, 'assertQueue');

If you just want to keep track of calls and other information use a spies, if you need to change behavior use a stubs.

@lucasfcosta Well, it turned out you were right. I wasn't aware that .spy would actually _call_ the original method you are spying on. I replaced most .stub calls with .spy and now all tests are passing.

I still have that little issue about the syntax update warning on things like:

  this.stub(seneca, 'export').withArgs('transport/utils').returns(transportUtils);

How should that be updated?

I actually need the callsFake behavior and it seems to fail for me, too. I mean the stubbed function is simply returning undefined. I've tried debugging it and it looks like the fake is recorded but this.fakes is still empty in the spy.

Going back to the original 3-args syntax works.

Hi @emirotin, can you please provide a reproducible example? I had a hard time reproducing this in a concise example, which makes things a lot easier for me to detect and fix possible bugs.

@nfantone Sorry for the delay on answering you, I've been very busy in the last days.
I tried to reproduce the warnings you mentioned by using the following test code and I couldn't get any warnings, both using the master branch and the tagged v2.0.0 commit:

var stub = sinon.stub;
var obj = {
  bla: function () { return 'foo' }
}

stub(obj, 'bla').withArgs('lol').returns('bar');

assert(obj.bla('lol'), 'bar')

Can you provide a small reproducible example so I can dig into this more deeply?

Huh I can't reproduce in the simplistic cases. That's really strange as I have a project where it's reproducible. Let me create a reduced version of it

Ah, here it is!

Looks like reset is erasing the fake:

var sinon = require('sinon')

var x = {
    a: function() {
        return 'Fail'
    }
}

var sandbox = sinon.sandbox.create();

sandbox.stub(x, 'a').callsFake(function() {
    return 'OK'
})

console.log(x.a())

sandbox.reset();

console.log(x.a()) // <-- undefined

Thanks @emirotin!

So, according to the docs I believe this is correct behavior, right?

Let's wait for @nfantone's answer so we can fix and hopefully close this.

Also, the docs for sandboxes seem to have markdown problems:

screen shot 2017-04-24 at 18 48 24

Do you know how can I fix it? I'm trying to edit both docs/_releases and docs/release-source but none of them seem to change the docs served by my local server.

I'm not sure it's correct even according to the docs. And definitely works against my intuition.
I reset to reset the _state_ of fakes, but not remove the fakes from the stub.
Like it's expected to reset the call counts, last args, last result, things like that.

My expectations from callsFake is that it's part of the setup and shouldn't be gone after reset which I read as "make everything look as right after the original setup".

And definitely it's different from the previous 3-arg form but it's advertised by the warning as the new equivalent to it.
But the old API is immune to resets.

@emirotin I agree with you regarding the name consistency.

I've commented the same thing at #1371. I think we must have consistent names for the reset API.

I'll be more than happy to make a PR for that if the project's maintainers agree with this change.

Let's wait to hear the maintainers' thoughts.

@lucasfcosta Sorry for the delay here.

Can you provide a small reproducible example so I can dig into this more deeply?

The test suite included in seneca-amqp-transport reproduces the issue and outputs the warning.

Just clone the repo, install deps, run npm test. You should see something like:

    the handleMessage() function
      ✓ should handle the request when a message is consumed
sinon.stub(obj, 'meth', fn) is deprecated and will be removed from the public API in a future version of sinon.
 Use stub(obj, 'meth').callsFake(fn).
 Codemod available at https://github.com/hurrymaplelad/sinon-codemod

That corresponds to the following test:

    it('should handle the request when a message is consumed',
      sinon.test(function(done) {
        var handleRequest = this.spy(transportUtils, 'handle_request');

       // This is the sub throwing the warning
        this.stub(seneca, 'export')
            .withArgs('transport/utils').returns(transportUtils);

        var listener = Listener(seneca, options);
        listener.listen()
          .then(() => {
            handleRequest.should.have.been.calledOnce();
            handleRequest.should.have.been.calledWith(seneca, { foo: 'bar' },
              DEFAULT_OPTIONS, sinon.match.func);
          })
          .asCallback(done);
      }));

I know it may not be a _small_ example, but it sure is real and reproducible.

On a side note, I agree with @emirotin sentiment on the .reset and .callsFake behaviour. If the new API is meant to be a "compatible replacement", that is.

Took me quite some time to figure out, because for me the simple replacement with .callsFake wasn't working, too. When replacing all reset() calls with resetHistory(), as @emirotin discovered, it actually works again. This should really be part of the deprecation/update guide.

This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions.

Promise.then not work in callsFake method?

// first way
// if no await will not arrived
await store.dispatch(deleteById(id)).then(()=>{
     // can arrived here
 })
// second way
sinon.stub(wrapper.find(Account).instance(), "_deleteById").callsFake(async (id)=>{
     // truly http request by axios
     await store.dispatch(deleteById(id)).then(()=>{
          // can't arrived here
      })
});

in aciton

export function deleteById(id){
    return dispatch=>{
        return axios.put(url,data).then(()=>{
              // can't arrived here second way but first arrived
        })
    }
}

@mqliutie it would appear so, at least in latest Chrome on MacOS, bumped into this in Cypress Graphql Stubbing (https://github.com/cypress-io/cypress-documentation/issues/122)

@MichaelHindley Call done() will work. I don't know is this right way

I don't think done() helps my particular use case since I'm stubbing the window.fetch function in order to load mocked GraphQL data and I must return a "thennable" Promise for apollo 😢 thanks for the input though!

I think I reopened this accidentally. Closing, as the docs explicitly mention this.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

tinganho picture tinganho  ·  3Comments

byohay picture byohay  ·  3Comments

fearphage picture fearphage  ·  3Comments

akdor1154 picture akdor1154  ·  4Comments

fearphage picture fearphage  ·  4Comments