Sinon: withArgs does not work properly with sinon.mock

Created on 30 Aug 2017  ·  3Comments  ·  Source: sinonjs/sinon

  • Sinon version : 3.2.1
  • Environment: OSX, Node 7
  • Other libraries you are using: Mocha, mjackson/expect

What did you expect to happen?
When I create a mock on a class and try to control the behavior of one of its functions using withArgs, an exception is thrown. Happens only when using multiple calls of withArgs.

What actually happens

How to reproduce


Reproducible example

Assume I declared a class C:

      class C {
        static foo(arg1, arg2) {return 'foo';}
      }

I try to control the behavior of foo using a mock on C:

      it('test withArgs mock', () => {
        let CMock = sinon.mock(C);
        let fooStub = CMock.expects('foo');

        fooStub.withArgs('a', 'b').returns(1);
        fooStub.withArgs('c', 'd').returns(2);

        expect(fooStub('a', 'b')).toEqual(1);
        expect(fooStub('c', 'd')).toEqual(2);

        CMock.restore();
      });

Which throws the following exception:

ExpectationError: foo received wrong arguments ["a", "b"], expected ["c", "d"]
at Object.fail (node_modules/sinon/lib/sinon/mock-expectation.js:281:25)
at Function. (node_modules/sinon/lib/sinon/mock-expectation.js:182:33)
at Array.forEach (native)
at Function.verifyCallAllowed (node_modules/sinon/lib/sinon/mock-expectation.js:175:27)
at Function.invoke (node_modules/sinon/lib/sinon/mock-expectation.js:78:14)
at proxy (node_modules/sinon/lib/sinon/spy.js:89:22)

While this code works:

      it('test withArgs stub', () => {
        let fooStub = sinon.stub(C, 'foo');

        fooStub.withArgs('a', 'b').returns(1);
        fooStub.withArgs('c', 'd').returns(2);

        expect(fooStub('a', 'b')).toEqual(1);
        expect(fooStub('c', 'd')).toEqual(2);

        fooStub.restore();
      });

It is worth mentioning that the following scenarios work as expected:

      it('test one withArgs mock', () => {
        let CMock = sinon.mock(C);
        let fooStub = CMock.expects('foo');

        fooStub.withArgs('a', 'b').returns(1);

        expect(fooStub('a', 'b')).toEqual(1); // ok

        CMock.restore();
      });
      it('test one withArgs mock', () => {
        let CMock = sinon.mock(C);
        let fooStub = CMock.expects('foo');

        fooStub.withArgs('a', 'b').returns(1);

        expect(fooStub('c', 'd')).toEqual(1); // error: foo received wrong arguments ["c", "d"], expected ["a", "b"]

        CMock.restore();
      });

Meaning, the problem occurs only when using withArgs multiple times.
It might be related to #1381, but as you can see in the example, the solution suggested there does not work.

Bug Medium Help wanted stale

Most helpful comment

I got a look at the code.
First of all, this test works:

class C {
  static foo(arg1, arg2) {return 'foo';}
}

it('test withArgs mock', () => {
  let CMock = sinon.mock(C);

  CMock.expects('foo').withArgs('a', 'b').returns(1);
  CMock.expects('foo').withArgs('c', 'd').returns(2);

  expect(C.foo('a', 'b')).toEqual(1);
  expect(C.foo('c', 'd')).toEqual(2);

  CMock.restore();
});

There are two reasons why this works and the test in the original issue doesn't:

  • I create two expects.
  • I call the static method instead of the one returned by expects.

Meaning, this doesn't work either:

it('test withArgs mock', () => {
  let CMock = sinon.mock(C);

  CMock.expects('foo').withArgs('a', 'b').returns(1);
  let fooStub = CMock.expects('foo').withArgs('c', 'd').returns(2);

  expect(fooStub('a', 'b')).toEqual(1); // Error: foo received wrong arguments ["a", "b"], expected ["c", "d"]
  expect(fooStub('c', 'd')).toEqual(2);

  CMock.restore();
});

Why does it happen?

Every time expects is called, the following happens:

  • A new expectation (stub enhanced with minCalls/maxCalls - which are the variables that are changed when you call once() for example) is created, only that it is a "scoped stub", it doesn't have connection to other expects. It is the object returned from expects.
  • It is added to the list of expectations.

Which results in different behaviors for these scenarios:

  • If you call the method returned by expects, it will be limited only to that expects.
  • If you call the method directly and not the stub, it will search over all the expects and find one that fits. If there is more than one, it will call the first.

Is this the correct behavior? It seems to me more intuitive that once someone calls expects on a method, it will behave exactly like a stub (with the additional minCalls/maxCalls). This will result in:

  • More code can be shared between stub and mock-expectation.
  • The behavior of the scenarios above will be the same.

Now, what would happen to minCalls/maxCalls?

Since there is only one expectation now, I propose three alternative ways to do call count verification with mocks:

  • Allow to concat once(), twice() etc.
  let fooStub = CMock.expects('foo').withArgs('a', 'b').returns(1).twice();
  fooStub.withArgs('c', 'd').returns(2).once(); // Does not affect the previous expectation.

Might be a bit confusing though.

  • Don't allow to concat once(), twice() etc.
  CMock.expects('foo').withArgs('a', 'b').returns(1).twice(); // `twice()` returns `null`, so that one has to call `expects()` again to set expectations for other arguments. 
  CMock.expects('foo').withArgs('c', 'd').returns(2).once();

More clear, but you cannot stub foo more than once so we will need a way to bypass that. Also, this is still possible:

  CMock.expects('foo').withArgs('a', 'b').returns(1).withArgs('c', 'd').returns(2).twice(); // I guess that this should only affect the last `withArgs`?
  • Use Spy API which remembers the number of calls.

All 3 comments

Thanks for a examplary issue report. I don't use the mocks functionality, as it makes my brain hurt, so someone else will need to have a look at this, but your examples should make that a lot easier.

Feel free to have a look at the code yourself - often the quickest way of getting this fixed :-)

I got a look at the code.
First of all, this test works:

class C {
  static foo(arg1, arg2) {return 'foo';}
}

it('test withArgs mock', () => {
  let CMock = sinon.mock(C);

  CMock.expects('foo').withArgs('a', 'b').returns(1);
  CMock.expects('foo').withArgs('c', 'd').returns(2);

  expect(C.foo('a', 'b')).toEqual(1);
  expect(C.foo('c', 'd')).toEqual(2);

  CMock.restore();
});

There are two reasons why this works and the test in the original issue doesn't:

  • I create two expects.
  • I call the static method instead of the one returned by expects.

Meaning, this doesn't work either:

it('test withArgs mock', () => {
  let CMock = sinon.mock(C);

  CMock.expects('foo').withArgs('a', 'b').returns(1);
  let fooStub = CMock.expects('foo').withArgs('c', 'd').returns(2);

  expect(fooStub('a', 'b')).toEqual(1); // Error: foo received wrong arguments ["a", "b"], expected ["c", "d"]
  expect(fooStub('c', 'd')).toEqual(2);

  CMock.restore();
});

Why does it happen?

Every time expects is called, the following happens:

  • A new expectation (stub enhanced with minCalls/maxCalls - which are the variables that are changed when you call once() for example) is created, only that it is a "scoped stub", it doesn't have connection to other expects. It is the object returned from expects.
  • It is added to the list of expectations.

Which results in different behaviors for these scenarios:

  • If you call the method returned by expects, it will be limited only to that expects.
  • If you call the method directly and not the stub, it will search over all the expects and find one that fits. If there is more than one, it will call the first.

Is this the correct behavior? It seems to me more intuitive that once someone calls expects on a method, it will behave exactly like a stub (with the additional minCalls/maxCalls). This will result in:

  • More code can be shared between stub and mock-expectation.
  • The behavior of the scenarios above will be the same.

Now, what would happen to minCalls/maxCalls?

Since there is only one expectation now, I propose three alternative ways to do call count verification with mocks:

  • Allow to concat once(), twice() etc.
  let fooStub = CMock.expects('foo').withArgs('a', 'b').returns(1).twice();
  fooStub.withArgs('c', 'd').returns(2).once(); // Does not affect the previous expectation.

Might be a bit confusing though.

  • Don't allow to concat once(), twice() etc.
  CMock.expects('foo').withArgs('a', 'b').returns(1).twice(); // `twice()` returns `null`, so that one has to call `expects()` again to set expectations for other arguments. 
  CMock.expects('foo').withArgs('c', 'd').returns(2).once();

More clear, but you cannot stub foo more than once so we will need a way to bypass that. Also, this is still possible:

  CMock.expects('foo').withArgs('a', 'b').returns(1).withArgs('c', 'd').returns(2).twice(); // I guess that this should only affect the last `withArgs`?
  • Use Spy API which remembers the number of calls.

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.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

fhd picture fhd  ·  23Comments

jonnyreeves picture jonnyreeves  ·  33Comments

ngerritsen picture ngerritsen  ·  26Comments

lavelle picture lavelle  ·  31Comments

fatso83 picture fatso83  ·  21Comments