Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement 'to have no calls satisfying' assertion #41

Merged
merged 2 commits into from
Jan 12, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
83 changes: 83 additions & 0 deletions documentation/assertions/spy/to-have-no-calls-satisfying.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
Passes if none of the calls to a spy [satisfy](http://unexpected.js.org/assertions/any/to-satisfy/) a given spec:

```js
var increment = sinon.spy(function increment(n) {
return n + 1;
});
increment(42);
increment(46);
expect(increment, 'to have no calls satisfying', { args: [ 43 ], returnValue: 44 });
```

In case of a failing expectation you get the following output:

```js
var quux = sinon.spy().named('quux');
quux(789);
quux(123, 456);

expect(quux, 'to have no calls satisfying', { args: [ 123, 456 ] });
```

```output
expected quux to have no calls satisfying { args: [ 123, 456 ] }

quux( 789 ); at theFunction (theFileName:xx:yy)
quux( 123, 456 ); at theFunction (theFileName:xx:yy) // should be removed
```

An array value will be interpreted as a shorthand for `{ args: ... }`:

```js
expect(quux, 'to have no calls satisfying', [ 123 ]);
```

Likewise for an object with only numerical properties:

```js
expect(quux, 'to have no calls satisfying', { 1: 789 });
```

Note that the individual parameters are matched with
[`to satisfy`](http://unexpected.js.org/assertions/any/to-satisfy/)
semantics, so the following fails despite the actual call
also had a `bar` property in the object passed as the first
parameter:

```js
var mySpy = sinon.spy().named('mySpy');
mySpy({foo: 123, bar: 456});
expect(mySpy, 'to have no calls satisfying', { args: [ { foo: 123 } ] });
```

```output
expected mySpy to have no calls satisfying { args: [ { foo: 123 } ] }

mySpy( { foo: 123, bar: 456 } ); at theFunction (theFileName:xx:yy) // should be removed
```

If that's not what you want, consider using the `exhaustively` flag. Then
the assertion passes because none of the calls matched exhaustively:

```js
expect(mySpy, 'to have no calls exhaustively satisfying', { args: [ { foo: 123 } ] });
```

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 no calls satisfying', function () {
foo(1);
});
```

```output
expected foo to have no calls satisfying foo( 1 );

foo( 1 ); at theFunction (theFileName:xx:yy) // should be removed
foo( 2 ); at theFunction (theFileName:xx:yy)
```
57 changes: 57 additions & 0 deletions lib/unexpected-sinon.js
Original file line number Diff line number Diff line change
Expand Up @@ -557,6 +557,63 @@
}
});

expect.addAssertion('<spy|array|sinonSandbox> to have no 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 no 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));
var promises = calls.map(function (call) {
return expect.promise(function () {
return expect(call, 'to [exhaustively] satisfy', value);
});
});
return expect.promise.settle(promises).then(function () {
var failed = promises.some(function (promise) {
return promise.isFulfilled();
});

if (failed) {
return expect(calls, 'to equal', toSpyCalls(calls.filter(function (call, i) {
return promises[i].isRejected();
})));
}
});
});

expect.addAssertion('<spy|array|sinonSandbox> to have no calls [exhaustively] satisfying <array>', function (expect, subject, value) {
return expect(subject, 'to have no calls [exhaustively] satisfying', { args: value });
});

expect.addAssertion('<spy|array|sinonSandbox> to have no 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 no calls satisfying', expectedSpyCallSpecs[0]);
});

expect.addAssertion('<spy|array|sinonSandbox> to have a call [exhaustively] satisfying <object>', function (expect, subject, value) {
var keys = Object.keys(value);
if (
Expand Down
188 changes: 188 additions & 0 deletions test/unexpected-sinon.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -471,6 +471,194 @@ describe('unexpected-sinon', function () {
});
});

describe('to have no calls satisfying', function () {
it('passes if the spy was never called with the provided arguments', function () {
spy('foo', 'true');
expect(spy, 'to have no calls satisfying', ['bar', expect.it('to be truthy')]);
});

it('fails if the spy was called with the provided arguments', function () {
expect(function () {
spy('bar', 'true');
expect(spy, 'to have no calls satisfying', ['bar', expect.it('to be truthy')]);
}, 'to throw exception',
"expected spy1 to have no calls satisfying [ 'bar', expect.it('to be truthy') ]\n" +
"\n" +
"spy1( 'bar', 'true' ); at theFunction (theFileName:xx:yy) // should be removed"
);
});

it('fails if the spy has a call that satisfies the criteria and another call that does not', function () {
expect(function () {
spy('foo');
spy('bar', {});
expect(spy, 'to have no calls satisfying', { 0: 'bar' });
}, 'to throw exception',
"expected spy1 to have no calls satisfying { 0: 'bar' }\n" +
"\n" +
"spy1( 'foo' ); at theFunction (theFileName:xx:yy)\n" +
"spy1( 'bar', {} ); at theFunction (theFileName:xx:yy) // should be removed"
);
});

describe('when passed a spec object', function () {
it('should succeed when no spy call satisfies the spec', function () {
spy(123, 456);
expect(spy, 'to have no calls satisfying', {
args: [ 789 ]
});
});

it('should fail when the spy was called with the provided parameters', function () {
spy(456);
spy(567);
expect(function () {
expect(spy, 'to have no calls satisfying', {
args: [ 456 ]
});
}, 'to throw',
"expected spy1 to have no calls satisfying { args: [ 456 ] }\n" +
"\n" +
"spy1( 456 ); at theFunction (theFileName:xx:yy) // should be removed\n" +
"spy1( 567 ); at theFunction (theFileName:xx:yy)"
);
});
});

describe('when passed an array (shorthand for {args: ...})', function () {
it('should succeed', function () {
spy(123, { foo: 'baz' });
expect(spy, 'to have no calls satisfying', [ 123, { foo: 'bar' } ]);
});

it('should fail with a diff', function () {
expect(function () {
spy(123, { foo: 'bar' });
expect(spy, 'to have no calls satisfying', [ 123, { foo: 'bar' } ]);
}, 'to throw',
"expected spy1 to have no calls satisfying [ 123, { foo: 'bar' } ]\n" +
"\n" +
"spy1( 123, { foo: 'bar' } ); at theFunction (theFileName:xx:yy) // should be removed"
);
});
});

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 no calls satisfying', {0: 123, 1: {foo: 'baz'}});
});

it('should fail with a diff', function () {
expect(function () {
spy(123, { foo: 'baz' });
expect(spy, 'to have no calls satisfying', {0: 123, 1: {foo: 'baz'}});
}, 'to throw',
"expected spy1 to have no calls satisfying { 0: 123, 1: { foo: 'baz' } }\n" +
"\n" +
"spy1( 123, { foo: 'baz' } ); at theFunction (theFileName:xx:yy) // should be removed"
);
});
});

describe('when passed a function that performs the expected call', function () {
it('should succeed when a spy call satisfies the spec', function () {
spy(123, 789);
expect(spy, 'to have no calls satisfying', function () {
spy(123, 456);
});
});

it('should fail if the function does not call the spy', function () {
expect(function () {
expect(spy, 'to have no calls satisfying', function () {});
}, 'to throw',
"expected spy1 to have no 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 no calls satisfying', function () {
spy(123);
spy(456);
});
}, 'to throw',
"expected spy1 to have no 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 with the given arguments', function () {
spy(123);
spy(456);
expect(function () {
expect(spy, 'to have no calls satisfying', function () {
spy(456);
});
}, 'to throw',
"expected spy1 to have no calls satisfying spy1( 456 );\n" +
"\n" +
"spy1( 123 ); at theFunction (theFileName:xx:yy)\n" +
"spy1( 456 ); at theFunction (theFileName:xx:yy) // should be removed"
);
});
});

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');
var spy2 = sandbox.spy().named('spy2');
spy1(123);
spy2(456);
return expect(sandbox, 'to have no calls satisfying', { spy: spy1, args: [ 789 ] });
});

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 no calls satisfying', { spy: spy1, args: [ 456 ] });
}, 'to error with',
"expected sinon sandbox to have no calls satisfying { spy: spy1, args: [ 456 ] }\n" +
"\n" +
"spy1( 456 ); at theFunction (theFileName:xx:yy) // should be removed"
);
});
});

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);
spy2(456);
return expect([spy1, spy2], 'to have no calls satisfying', { spy: spy1, args: [ 789 ] });
});

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 no calls satisfying', { spy: spy1, args: [ 123 ] });
}, 'to error with',
"expected [ spy1, spy2 ] to have no calls satisfying { spy: spy1, args: [ 123 ] }\n" +
"\n" +
"spy1( 123 ); at theFunction (theFileName:xx:yy) // should be removed\n" +
"spy2( 456 ); at theFunction (theFileName:xx:yy)"
);
});
});
});

describe('was called with exactly', function () {
it('passes if the spy was called with the provided arguments and no others', function () {
spy('foo', 'bar', 'baz');
Expand Down