Skip to content

Commit

Permalink
Add support for asserting on sinon.createStubInstance(Constructor);
Browse files Browse the repository at this point in the history
(or any object that has spies as direct or prototypal properties).

Applies to these assertions:

    to have calls [exhaustively] satisfying
    to have a call [exhaustively] satisfying
    to have no calls [exhaustively] satisfying

Gathers all spies attached to the object and asserts on the complete
timeline.
  • Loading branch information
papandreou committed Jan 24, 2017
1 parent a65387a commit b5297d5
Show file tree
Hide file tree
Showing 3 changed files with 226 additions and 17 deletions.
92 changes: 76 additions & 16 deletions lib/unexpected-sinon.js
Original file line number Diff line number Diff line change
Expand Up @@ -428,15 +428,60 @@
}
});

function extractSpiesFromSinonStubInstance(stubInstance) {
var spies = [];
for (var propertyName in stubInstance) {
if (isSpy(stubInstance[propertyName])) {
spies.push(stubInstance[propertyName]);
}
}
if (spies.length === 0) {
throw new Error('The passed object was not recognized as a sinon "stub instance" as it has no spies attached to it');
}
return spies;
}

function extractSpies(spyOrArrayOrSinonSandbox) {
if (expect.findTypeOf(spyOrArrayOrSinonSandbox).is('sinonSandbox')) {
return spyOrArrayOrSinonSandbox.fakes || [];
} else if (Array.isArray(spyOrArrayOrSinonSandbox)) {
return spyOrArrayOrSinonSandbox;
} else if (spyOrArrayOrSinonSandbox && typeof spyOrArrayOrSinonSandbox === 'object') {
return extractSpiesFromSinonStubInstance(spyOrArrayOrSinonSandbox);
} else {
return Array.isArray(spyOrArrayOrSinonSandbox) ? spyOrArrayOrSinonSandbox : [ spyOrArrayOrSinonSandbox ];
// Assume spy
return [ spyOrArrayOrSinonSandbox ];
}
}

function overrideSubjectOutputIfStubInstance(subject, expect, spiesIfAvailable) {
if (expect.subjectType.name === 'object') {
// Replace "stubbed instance" subject with: ClassName({spy1, spy2, spy3 /* 2 more */})
expect.subjectOutput = function (output) {
var spies = spiesIfAvailable || extractSpies(subject);
output.jsFunctionName(subject.constructor.name || 'sinonStubInstance')
.text('({');
var width = 0;
for (var i = 0 ; i < spies.length ; i += 1) {
var spy = spies[i];
var itemWidth = (i > 0 ? 2 : 0) + (spy.displayName ? spy.displayName.length : 4);
if ((width + itemWidth) < (expect.output.preferredWidth - 40)) {
if (i > 0) {
output.text(',').sp();
}
output.appendInspected(spy);
width += itemWidth;
} else {
output.sp().jsComment('/* ' + (spies.length - i) + ' more */');
break;
}
}
output.text('})');
};
}
}

expect.addAssertion('<spy|array|sinonSandbox> to have calls [exhaustively] satisfying <function>', function (expect, subject, value) {
expect.addAssertion('<spy|array|sinonSandbox|object> to have calls [exhaustively] satisfying <function>', function (expect, subject, value) {
var spies = extractSpies(subject);
var expectedSpyCallSpecs = recordSpyCalls(spies, value);
var expectedSpyCalls = [];
Expand All @@ -448,10 +493,11 @@
expect.argsOutput[0] = function (output) {
output.appendInspected(expectedSpyCalls);
};
return expect(spies, 'to have calls [exhaustively] satisfying', expectedSpyCallSpecs);
overrideSubjectOutputIfStubInstance(subject, expect, spies);
return expect(subject, 'to have calls [exhaustively] satisfying', expectedSpyCallSpecs);
});

expect.addAssertion('<spy|array|sinonSandbox> to have calls [exhaustively] satisfying <array|object>', function (expect, subject, value) {
expect.addAssertion('<spy|array|sinonSandbox|object> to have calls [exhaustively] satisfying <array|object>', function (expect, subject, value) {
var spies = extractSpies(subject);
var spyCalls = [];
var isSeenBySpyId = {};
Expand All @@ -465,6 +511,8 @@
return a.callId - b.callId;
});

overrideSubjectOutputIfStubInstance(subject, expect, spies);

var seenSpies = [];
function wrapSpyInObject(obj) {
if (isSpy(obj)) {
Expand Down Expand Up @@ -564,8 +612,9 @@
}
});

expect.addAssertion('<spy|array|sinonSandbox> to have no calls [exhaustively] satisfying <object>', function (expect, subject, value) {
expect.addAssertion('<spy|array|sinonSandbox|object> to have no calls [exhaustively] satisfying <object>', function (expect, subject, value) {
var spies = extractSpies(subject);
overrideSubjectOutputIfStubInstance(subject, expect, spies);
var keys = Object.keys(value);
if (
keys.length > 0 &&
Expand Down Expand Up @@ -598,12 +647,15 @@
});
});

expect.addAssertion('<spy|array|sinonSandbox> to have no calls [exhaustively] satisfying <array>', function (expect, subject, value) {
expect.addAssertion('<spy|array|sinonSandbox|object> to have no calls [exhaustively] satisfying <array>', function (expect, subject, value) {
overrideSubjectOutputIfStubInstance(subject, expect);
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);
expect.addAssertion('<spy|array|sinonSandbox|object> to have no calls satisfying <function>', function (expect, subject, value) {
var spies = extractSpies(subject);
overrideSubjectOutputIfStubInstance(subject, expect, spies);
var expectedSpyCallSpecs = recordSpyCalls(spies, value);
var expectedSpyCalls = [];
expectedSpyCallSpecs.forEach(function (expectedSpyCallSpec) {
expectedSpyCalls.push(expectedSpyCallSpec.call);
Expand All @@ -622,8 +674,9 @@
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) {
expect.addAssertion('<spy|array|sinonSandbox|object> to have a call [exhaustively] satisfying <object>', function (expect, subject, value) {
var spies = extractSpies(subject);
overrideSubjectOutputIfStubInstance(subject, expect, spies);
var keys = Object.keys(value);
if (
keys.length > 0 &&
Expand Down Expand Up @@ -654,12 +707,15 @@
});
});

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

expect.addAssertion('<spy|array|sinonSandbox> to have a call satisfying <function>', function (expect, subject, value) {
var expectedSpyCallSpecs = recordSpyCalls(extractSpies(subject), value);
expect.addAssertion('<spy|array|sinonSandbox|object> to have a call satisfying <function>', function (expect, subject, value) {
var spies = extractSpies(subject);
overrideSubjectOutputIfStubInstance(subject, expect, spies);
var expectedSpyCallSpecs = recordSpyCalls(spies, value);
var expectedSpyCalls = [];
expectedSpyCallSpecs.forEach(function (expectedSpyCallSpec) {
expectedSpyCalls.push(expectedSpyCallSpec.call);
Expand All @@ -678,8 +734,9 @@
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) {
expect.addAssertion('<spy|array|sinonSandbox|object> to have all calls [exhaustively] satisfying <object>', function (expect, subject, value) {
var spies = extractSpies(subject);
overrideSubjectOutputIfStubInstance(subject, expect, spies);
var keys = Object.keys(value);
if (
keys.length > 0 &&
Expand All @@ -695,12 +752,15 @@
return expect(getCallTimeLineFromSpies(spies), 'to have items [exhaustively] satisfying', value);
});

expect.addAssertion('<spy|array|sinonSandbox> to have all calls [exhaustively] satisfying <array>', function (expect, subject, value) {
expect.addAssertion('<spy|array|sinonSandbox|object> to have all calls [exhaustively] satisfying <array>', function (expect, subject, value) {
overrideSubjectOutputIfStubInstance(subject, expect);
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);
expect.addAssertion('<spy|array|sinonSandbox|object> to have all calls satisfying <function>', function (expect, subject, value) {
var spies = extractSpies(subject);
overrideSubjectOutputIfStubInstance(subject, expect, spies);
var expectedSpyCallSpecs = recordSpyCalls(spies, value);
var expectedSpyCalls = [];
expectedSpyCallSpecs.forEach(function (expectedSpyCallSpec) {
expectedSpyCalls.push(expectedSpyCallSpec.call);
Expand Down
13 changes: 12 additions & 1 deletion test/monkeyPatchSinonStackFrames.js
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@

['spy', 'stub'].forEach(function (name) {
var orig = sinon[name];
sinon[name] = function () {
sinon[name] = function () { // ...
var result = orig.apply(this, arguments);
if (isSpy(result)) {
patchSpy(result);
Expand All @@ -49,4 +49,15 @@
};
sinon[name].create = orig.create;
});

var origCreateStubInstance = sinon.createStubInstance;
sinon.createStubInstance = function () { // ...
var instance = origCreateStubInstance.apply(this, arguments);
for (var propertyName in instance) {
if (isSpy(instance[propertyName])) {
patchSpy(instance[propertyName]);
}
}
return instance;
};
}));
138 changes: 138 additions & 0 deletions test/unexpected-sinon.spec.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,16 @@
/*global describe, it, beforeEach, sinon, unexpected*/

// Bogus class to be used with sinon.createStubInstance:
function MyClass() {
throw new Error('oh no');
}
MyClass.prototype.foo = function () {
throw new Error('oh no');
};
MyClass.prototype.bar = function () {
throw new Error('oh no');
};

describe('unexpected-sinon', function () {
var expect, spy;

Expand Down Expand Up @@ -520,6 +532,34 @@ describe('unexpected-sinon', function () {
);
});

describe('when passed a sinon stub instance as the subject', function () {
it('should succeed', function () {
var stubInstance = sinon.createStubInstance(MyClass);
stubInstance.foo(123);
return expect(stubInstance, 'to have no calls satisfying', function () {
stubInstance.foo(456);
});
});

it('should fail with a diff', function () {
var stubInstance = sinon.createStubInstance(MyClass);
stubInstance.foo(123);
stubInstance.bar(456);
stubInstance.foo(123);
return expect(function () {
return expect(stubInstance, 'to have no calls satisfying', function () {
stubInstance.bar(456);
});
}, 'to error with',
"expected MyClass({foo, bar}) to have no calls satisfying bar( 456 );\n" +
"\n" +
"foo( 123 ); at theFunction (theFileName:xx:yy)\n" +
"bar( 456 ); at theFunction (theFileName:xx:yy) // should be removed\n" +
"foo( 123 ); at theFunction (theFileName:xx:yy)"
);
});
});

describe('when passed a spec object', function () {
it('should succeed when no spy call satisfies the spec', function () {
spy(123, 456);
Expand Down Expand Up @@ -908,6 +948,36 @@ describe('unexpected-sinon', function () {
});

describe('to have a call satisfying', function () {
describe('when passed a sinon stub instance as the subject', function () {
it('should succeed', function () {
var stubInstance = sinon.createStubInstance(MyClass);
stubInstance.foo(123);
stubInstance.foo(456);
return expect(stubInstance, 'to have a call satisfying', function () {
stubInstance.foo(123);
});
});

it('should fail with a diff', function () {
var stubInstance = sinon.createStubInstance(MyClass);
stubInstance.foo(123);
stubInstance.bar(456);
stubInstance.foo(123);
return expect(function () {
return expect(stubInstance, 'to have a call satisfying', function () {
stubInstance.bar(789);
});
}, 'to error with',
"expected MyClass({foo, bar}) to have a call satisfying bar( 789 );\n" +
"\n" +
"foo( 123 ); at theFunction (theFileName:xx:yy) // should be bar( 789 );\n" +
"bar(\n" +
" 456 // should equal 789\n" +
"); at theFunction (theFileName:xx:yy)\n" +
"foo( 123 ); at theFunction (theFileName:xx:yy) // should be bar( 789 );"
);
});
});
describe('when passed a spec object', function () {
it('should succeed when a spy call satisfies the spec', function () {
spy(123, 456);
Expand Down Expand Up @@ -1137,6 +1207,35 @@ describe('unexpected-sinon', function () {
});

describe('to have all calls satisfying', function () {
describe('when passed a sinon stub instance as the subject', function () {
it('should succeed', function () {
var stubInstance = sinon.createStubInstance(MyClass);
stubInstance.foo(123);
stubInstance.foo(123);
return expect(stubInstance, 'to have all calls satisfying', function () {
stubInstance.foo(123);
});
});

it('should fail with a diff', function () {
var stubInstance = sinon.createStubInstance(MyClass);
stubInstance.foo(123);
stubInstance.bar(456);
stubInstance.foo(123);
return expect(function () {
return expect(stubInstance, 'to have all calls satisfying', function () {
stubInstance.bar(456);
});
}, 'to error with',
"expected MyClass({foo, bar}) to have all calls satisfying bar( 456 );\n" +
"\n" +
"foo( 123 ); at theFunction (theFileName:xx:yy) // should be bar( 456 );\n" +
"bar( 456 ); at theFunction (theFileName:xx:yy)\n" +
"foo( 123 ); at theFunction (theFileName:xx:yy) // should be bar( 456 );"
);
});
});

describe('when passed a spec object', function () {
it('should succeed when a spy call satisfies the spec', function () {
spy(123, 456);
Expand Down Expand Up @@ -1487,6 +1586,45 @@ describe('unexpected-sinon', function () {
);
});

describe('when passed a sinon stub instance as the subject', function () {
it('should succeed', function () {
var stubInstance = sinon.createStubInstance(MyClass);
stubInstance.foo(123);
stubInstance.bar(456);
stubInstance.foo(789);
return expect(stubInstance, 'to have calls satisfying', function () {
stubInstance.foo(123);
stubInstance.bar(456);
stubInstance.foo(789);
});
});

it('should fail with a diff', function () {
var stubInstance = sinon.createStubInstance(MyClass);
stubInstance.foo(123);
stubInstance.bar(456);
stubInstance.foo(123);
return expect(function () {
return expect(stubInstance, 'to have calls satisfying', function () {
stubInstance.foo(123);
stubInstance.bar(123);
stubInstance.foo(123);
});
}, 'to error with',
"expected MyClass({foo, bar}) to have calls satisfying\n" +
"foo( 123 );\n" +
"bar( 123 );\n" +
"foo( 123 );\n" +
"\n" +
"foo( 123 ); at theFunction (theFileName:xx:yy)\n" +
"bar(\n" +
" 456 // should equal 123\n" +
"); at theFunction (theFileName:xx:yy)\n" +
"foo( 123 ); at theFunction (theFileName:xx:yy)"
);
});
});

describe('when passed an array entry (shorthand for {args: ...})', function () {
it('should succeed', function () {
spy(123, { foo: 'bar' });
Expand Down

0 comments on commit b5297d5

Please sign in to comment.