Skip to content

Commit

Permalink
Merge pull request #41 from unexpectedjs/feature/toHaveNoCallsSatisfying
Browse files Browse the repository at this point in the history
Implement 'to have no calls satisfying' assertion
  • Loading branch information
papandreou committed Jan 12, 2017
2 parents 280e701 + cd19904 commit db0b628
Show file tree
Hide file tree
Showing 3 changed files with 328 additions and 0 deletions.
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

0 comments on commit db0b628

Please sign in to comment.