Skip to content

Commit

Permalink
Implement 'to have all calls satisfying' assertion (#32).
Browse files Browse the repository at this point in the history
  • Loading branch information
papandreou committed Jan 12, 2017
1 parent 280e701 commit 4745d3e
Show file tree
Hide file tree
Showing 3 changed files with 366 additions and 0 deletions.
104 changes: 104 additions & 0 deletions 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)
```
42 changes: 42 additions & 0 deletions lib/unexpected-sinon.js
Expand Up @@ -612,6 +612,48 @@
return expect(subject, 'to have a call satisfying', expectedSpyCallSpecs[0]);
});

expect.addAssertion('<spy|array|sinonSandbox> to have all calls [exhaustively] satisfying <object>', 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('<spy|array|sinonSandbox> to have all calls [exhaustively] satisfying <array>', function (expect, subject, value) {
return expect(subject, 'to have all calls [exhaustively] satisfying', { args: value });
});

expect.addAssertion('<spy|array|sinonSandbox> to have all calls satisfying <function>', 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('<spy> was [always] called with [exactly] <any*>', function (expect, subject) {
expect.errorMode = 'defaultOrNested';
var args;
Expand Down
220 changes: 220 additions & 0 deletions test/unexpected-sinon.spec.js
Expand Up @@ -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);
Expand Down

0 comments on commit 4745d3e

Please sign in to comment.