sinonjs / sinon

Test spies, stubs and mocks for JavaScript.
https://sinonjs.org/
Other
9.66k stars 772 forks source link

New stub API failing after replacing calls with .callsFake #1341

Closed nfantone closed 6 years ago

nfantone commented 7 years ago

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 git@github.com:senecajs/seneca-amqp-transport.git
cd seneca-amqp-transport && npm i
npm run validate
fatso83 commented 7 years ago

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.

nfantone commented 7 years ago

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

nfantone commented 7 years ago

@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.

nfantone commented 7 years ago

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.

lucasfcosta commented 7 years ago

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.

nfantone commented 7 years ago

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?

lucasfcosta commented 7 years ago

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.

nfantone commented 7 years ago

@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?

emirotin commented 7 years ago

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.

lucasfcosta commented 7 years ago

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?

emirotin commented 7 years ago

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

emirotin commented 7 years ago

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
lucasfcosta commented 7 years ago

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.

emirotin commented 7 years ago

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.

lucasfcosta commented 7 years ago

@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.

nfantone commented 7 years ago

@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.

deyhle commented 7 years ago

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.

stale[bot] commented 6 years ago

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.

mqliutie commented 6 years ago

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
        })
    }
}
MichaelHindley commented 6 years ago

@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)

mqliutie commented 6 years ago

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

MichaelHindley commented 6 years ago

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!

fatso83 commented 6 years ago

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