diff --git a/documentation/assertions/spy/to-have-all-calls-satisfying.md b/documentation/assertions/spy/to-have-all-calls-satisfying.md new file mode 100644 index 0000000..37c722b --- /dev/null +++ b/documentation/assertions/spy/to-have-all-calls-satisfying.md @@ -0,0 +1,104 @@ +Passes if all calls to a spy [satisfy](http://unexpected.js.org/assertions/any/to-satisfy/) a given spec. +Also, the spy must have been called at least once for the assertion to pass. +Compare with the [to have a call satisfying](../to-have-a-call-satisfying/), +which only requires one call to satisfying the expectation. + +```js +var increment = sinon.spy(function increment(n) { + return n + 1; +}); +increment(42); +increment(42); +expect(increment, 'to have all calls satisfying', { args: [ 42 ], returnValue: 43 }); +``` + +In case of a failing expectation you get the following output: + +```js +var quux = sinon.spy().named('quux'); +quux('foo', 456); +quux(123, 456); + +expect(quux, 'to have all calls satisfying', { args: [ 'foo', 456 ] }); +``` + +```output +expected quux to have all calls satisfying { args: [ 'foo', 456 ] } + +quux( 'foo', 456 ); at theFunction (theFileName:xx:yy) +quux( + 123, // should equal 'foo' + 456 +); at theFunction (theFileName:xx:yy) +``` + +An array value will be interpreted as a shorthand for `{ args: ... }`: + +```js +var baz = sinon.spy().named('baz'); +baz(123, 456); + +expect(baz, 'to have all calls satisfying', [ 123, 456 ]); +``` + +Likewise for an object with only numerical properties: + +```js +expect(baz, 'to have a call satisfying', { 1: 456 }); +``` + +Note that the individual arguments are matched with +[`to satisfy`](http://unexpected.js.org/assertions/any/to-satisfy/) +semantics, which means that objects are allowed to have more properties than you +specify, so the following passes: + +```js +var mySpy = sinon.spy().named('mySpy'); +mySpy({foo: 123, bar: 456}); +mySpy({foo: 123, baz: 789}); +expect(mySpy, 'to have all calls satisfying', { args: [ { foo: 123 } ] }); +``` + +If that's not what you want, consider using the `exhaustively` flag: + +```js +expect(mySpy, 'to have all calls exhaustively satisfying', { args: [ { foo: 123 } ] }); +``` + +```output +expected mySpy to have all calls exhaustively satisfying { args: [ { foo: 123 } ] } + +mySpy( + { + foo: 123, + bar: 456 // should be removed + } +); at theFunction (theFileName:xx:yy) +mySpy( + { + foo: 123, + baz: 789 // should be removed + } +); at theFunction (theFileName:xx:yy) +``` + +You can also specify the expected call as a function that performs it: + +```js +var foo = sinon.spy().named('foo'); +foo(1); +foo(2); + +expect(foo, 'to have all calls satisfying', function () { + foo(1); +}); +``` + +```output +expected foo to have all calls satisfying foo( 1 ); + +foo( 1 ); at theFunction (theFileName:xx:yy) +foo( + 2 // should equal 1 +); at theFunction (theFileName:xx:yy) +``` diff --git a/lib/unexpected-sinon.js b/lib/unexpected-sinon.js index 11995e5..c6baafd 100644 --- a/lib/unexpected-sinon.js +++ b/lib/unexpected-sinon.js @@ -612,6 +612,48 @@ return expect(subject, 'to have a call satisfying', expectedSpyCallSpecs[0]); }); + expect.addAssertion(' to have all calls [exhaustively] satisfying ', function (expect, subject, value) { + var keys = Object.keys(value); + if ( + keys.length > 0 && + keys.every(function (key) { + return /^[1-9]*[0-9]+$/.test(key); + }) + ) { + return expect(subject, 'to have all calls [exhaustively] satisfying', { args: value }); + } + + expect.errorMode = 'defaultOrNested'; + + // Get a flattened array of all calls to all the specified spies: + var calls = Array.prototype.concat.apply([], extractSpies(subject).map(getCalls)); + return expect(calls, 'to have items [exhaustively] satisfying', value); + }); + + expect.addAssertion(' to have all calls [exhaustively] satisfying ', function (expect, subject, value) { + return expect(subject, 'to have all calls [exhaustively] satisfying', { args: value }); + }); + + expect.addAssertion(' to have all calls satisfying ', function (expect, subject, value) { + var expectedSpyCallSpecs = recordSpyCalls(extractSpies(subject), value); + var expectedSpyCalls = []; + expectedSpyCallSpecs.forEach(function (expectedSpyCallSpec) { + expectedSpyCalls.push(expectedSpyCallSpec.call); + delete expectedSpyCallSpec.call; + delete expectedSpyCallSpec.callId; + }); + if (expectedSpyCalls.length > 0) { + expect.argsOutput[0] = function (output) { + output.appendInspected(expectedSpyCalls); + }; + } + if (expectedSpyCallSpecs.length !== 1) { + expect.errorMode = 'nested'; + expect.fail('expected the provided function to call the spy exactly once, but it called it ' + expectedSpyCallSpecs.length + ' times'); + } + return expect(subject, 'to have all calls satisfying', expectedSpyCallSpecs[0]); + }); + expect.addAssertion(' was [always] called with [exactly] ', function (expect, subject) { expect.errorMode = 'defaultOrNested'; var args; diff --git a/test/unexpected-sinon.spec.js b/test/unexpected-sinon.spec.js index 7984195..0f79d37 100644 --- a/test/unexpected-sinon.spec.js +++ b/test/unexpected-sinon.spec.js @@ -929,6 +929,226 @@ describe('unexpected-sinon', function () { }); }); + describe('to have all calls satisfying', function () { + describe('when passed a spec object', function () { + it('should succeed when a spy call satisfies the spec', function () { + spy(123, 456); + expect(spy, 'to have all calls satisfying', { + args: [ 123, 456 ] + }); + }); + + it('should fail when the spy was not called at all', function () { + expect(function () { + expect(spy, 'to have all calls satisfying', { + args: [ 123, 456 ] + }); + }, 'to throw', + "expected spy1 to have all calls satisfying { args: [ 123, 456 ] }\n" + + " expected [] to have items satisfying { args: [ 123, 456 ] }\n" + + " expected [] to be non-empty" + ); + }); + + it('should fail when one of the calls had the wrong arguments', function () { + spy(456); + spy(567); + expect(function () { + expect(spy, 'to have all calls satisfying', { + args: [ 456 ] + }); + }, 'to throw', + "expected spy1 to have all calls satisfying { args: [ 456 ] }\n" + + "\n" + + "spy1( 456 ); at theFunction (theFileName:xx:yy)\n" + + "spy1(\n" + + " 567 // should equal 456\n" + + "); at theFunction (theFileName:xx:yy)" + ); + }); + + describe('with the exhaustively flag', function () { + it('should succeed when a spy call satisfies the spec', function () { + spy(123, { foo: 'bar' }); + expect(spy, 'to have all calls satisfying', { + args: [ 123, { foo: 'bar' } ] + }); + }); + + it('should fail when a spy call does not satisfy the spec only because of the "exhaustively" semantics', function () { + spy(123, { foo: 'bar', quux: 'baz' }); + expect(function () { + expect(spy, 'to have all calls exhaustively satisfying', { + args: [ 123, { foo: 'bar' } ] + }); + }, 'to throw', + "expected spy1 to have all calls exhaustively satisfying { args: [ 123, { foo: 'bar' } ] }\n" + + "\n" + + "spy1(\n" + + " 123,\n" + + " {\n" + + " foo: 'bar',\n" + + " quux: 'baz' // should be removed\n" + + " }\n" + + "); at theFunction (theFileName:xx:yy)" + ); + }); + }); + }); + + describe('when passed an array (shorthand for {args: ...})', function () { + it('should succeed', function () { + spy(123, { foo: 'bar' }); + expect(spy, 'to have all calls satisfying', [ 123, { foo: 'bar' } ]); + }); + + it('should fail with a diff', function () { + expect(function () { + spy(123, { foo: 'bar' }); + expect(spy, 'to have all calls satisfying', [ 123, { foo: 'baz' } ]); + }, 'to throw', + "expected spy1 to have all calls satisfying [ 123, { foo: 'baz' } ]\n" + + "\n" + + "spy1(\n" + + " 123,\n" + + " {\n" + + " foo: 'bar' // should equal 'baz'\n" + + " //\n" + + " // -bar\n" + + " // +baz\n" + + " }\n" + + "); at theFunction (theFileName:xx:yy)" + ); + }); + }); + + describe('when passed an array with only numerical properties (shorthand for {args: ...})', function () { + it('should succeed', function () { + spy(123, { foo: 'bar' }); + expect(spy, 'to have all calls satisfying', {0: 123, 1: {foo: 'bar'}}); + }); + + it('should fail with a diff', function () { + expect(function () { + spy(123, { foo: 'bar' }); + expect(spy, 'to have all calls satisfying', {0: 123, 1: {foo: 'baz'}}); + }, 'to throw', + "expected spy1 to have all calls satisfying { 0: 123, 1: { foo: 'baz' } }\n" + + "\n" + + "spy1(\n" + + " 123,\n" + + " {\n" + + " foo: 'bar' // should equal 'baz'\n" + + " //\n" + + " // -bar\n" + + " // +baz\n" + + " }\n" + + "); at theFunction (theFileName:xx:yy)" + ); + }); + }); + + describe('when passed a function that performs the expected call', function () { + it('should succeed when a spy call satisfies the spec', function () { + spy(123, 456); + expect(spy, 'to have all calls satisfying', function () { + spy(123, 456); + }); + }); + + it('should fail if the function does not call the spy', function () { + expect(function () { + expect(spy, 'to have all calls satisfying', function () {}); + }, 'to throw', + "expected spy1 to have all calls satisfying function () {}\n" + + " expected the provided function to call the spy exactly once, but it called it 0 times" + ); + }); + + it('should fail if the function calls the spy more than once', function () { + expect(function () { + expect(spy, 'to have all calls satisfying', function () { + spy(123); + spy(456); + }); + }, 'to throw', + "expected spy1 to have all calls satisfying\n" + + "spy1( 123 );\n" + + "spy1( 456 );\n" + + " expected the provided function to call the spy exactly once, but it called it 2 times" + ); + }); + + it('should fail when the spy was called, but one of the calls had the wrong arguments', function () { + spy(123); + spy(456); + expect(function () { + expect(spy, 'to have all calls satisfying', function () { + spy(123); + }); + }, 'to throw', + "expected spy1 to have all calls satisfying spy1( 123 );\n" + + "\n" + + "spy1( 123 ); at theFunction (theFileName:xx:yy)\n" + + "spy1(\n" + + " 456 // should equal 123\n" + + "); at theFunction (theFileName:xx:yy)" + ); + }); + }); + + describe('when passed a sinon sandbox as the subject', function () { + it('should succeed', function () { + var sandbox = sinon.sandbox.create(); + var spy1 = sandbox.spy().named('spy1'); + sandbox.spy().named('spy2'); + spy1(123); + spy1(123); + return expect(sandbox, 'to have all calls satisfying', { spy: spy1, args: [ 123 ] }); + }); + + it('should fail with a diff', function () { + var sandbox = sinon.sandbox.create(); + var spy1 = sandbox.spy().named('spy1'); + spy1(456); + return expect(function () { + return expect(sandbox, 'to have all calls satisfying', { spy: spy1, args: [ 123 ] }); + }, 'to error with', + "expected sinon sandbox to have all calls satisfying { spy: spy1, args: [ 123 ] }\n" + + "\n" + + "spy1(\n" + + " 456 // should equal 123\n" + + "); at theFunction (theFileName:xx:yy)" + ); + }); + }); + + describe('when passed an array of spies as the subject', function () { + it('should succeed', function () { + var spy1 = sinon.spy().named('spy1'); + var spy2 = sinon.spy().named('spy2'); + spy1(123); + return expect([spy1, spy2], 'to have all calls satisfying', { spy: spy1, args: [ 123 ] }); + }); + + it('should fail with a diff', function () { + var sandbox = sinon.sandbox.create(); + var spy1 = sandbox.spy().named('spy1'); + var spy2 = sandbox.spy().named('spy2'); + spy1(123); + spy2(456); + return expect(function () { + return expect([spy1, spy2], 'to have all calls satisfying', { spy: spy1, args: [ 123 ] }); + }, 'to error with', + "expected [ spy1, spy2 ] to have all calls satisfying { spy: spy1, args: [ 123 ] }\n" + + "\n" + + "spy1( 123 ); at theFunction (theFileName:xx:yy)\n" + + "spy2( 456 ); at theFunction (theFileName:xx:yy) // should be spy1( 123 );" + ); + }); + }); + }); + describe('to have calls satisfying', function () { it('should complain if the args value is passed as an object with non-numerical properties', function () { spy(123);