toHaveNoOtherSpyInteractions implementation

This commit is contained in:
Eradev
2025-01-10 21:05:12 -05:00
parent 7683325d68
commit 4732012f1c
16 changed files with 354 additions and 12 deletions

View File

@@ -675,6 +675,23 @@ describe('Matchers (Integration)', function() {
});
});
describe('toHaveNoOtherSpyInteractions', function() {
let spyObj;
beforeEach(function() {
spyObj = env.createSpyObj('NewClass', ['spyA', 'spyB']);
});
verifyPasses(function(env) {
env.expect(spyObj).toHaveNoOtherSpyInteractions();
});
verifyFails(function(env) {
spyObj.spyA();
env.expect(spyObj).toHaveNoOtherSpyInteractions();
});
});
describe('toMatch', function() {
verifyPasses(function(env) {
env.expect('foo').toMatch(/oo$/);

View File

@@ -112,4 +112,20 @@ describe('toHaveBeenCalledBefore', function() {
'Expected spy first spy to not have been called before spy second spy, but it was'
);
});
it('set the correct calls as verified when passing', function() {
const matcher = jasmineUnderTest.matchers.toHaveBeenCalledBefore(),
firstSpy = new jasmineUnderTest.Spy('first spy'),
secondSpy = new jasmineUnderTest.Spy('second spy');
firstSpy();
secondSpy();
matcher.compare(firstSpy, secondSpy);
expect(firstSpy.calls.count()).toBe(1);
expect(firstSpy.calls.unverifiedCount()).toBe(0);
expect(secondSpy.calls.count()).toBe(1);
expect(secondSpy.calls.unverifiedCount()).toBe(0);
});
});

View File

@@ -105,4 +105,18 @@ describe('toHaveBeenCalledOnceWith', function() {
matcher.compare(fn);
}).toThrowError(/Expected a spy, but got Function./);
});
it('set the correct calls as verified when passing', function() {
const pp = jasmineUnderTest.makePrettyPrinter(),
util = new jasmineUnderTest.MatchersUtil({ pp: pp }),
matcher = jasmineUnderTest.matchers.toHaveBeenCalledOnceWith(util),
calledSpy = new jasmineUnderTest.Spy('called-spy');
calledSpy('x');
matcher.compare(calledSpy, 'x');
expect(calledSpy.calls.count()).toBe(1);
expect(calledSpy.calls.unverifiedCount()).toBe(0);
});
});

View File

@@ -50,4 +50,16 @@ describe('toHaveBeenCalled', function() {
'Expected spy sample-spy to have been called.'
);
});
it('set the correct calls as verified when passing', function() {
const matcher = jasmineUnderTest.matchers.toHaveBeenCalled(),
spy = new jasmineUnderTest.Spy('sample-spy');
spy();
matcher.compare(spy);
expect(spy.calls.count()).toBe(1);
expect(spy.calls.unverifiedCount()).toBe(0);
});
});

View File

@@ -87,4 +87,17 @@ describe('toHaveBeenCalledTimes', function() {
' times.'
);
});
it('set the correct calls as verified when passing', function() {
const matcher = jasmineUnderTest.matchers.toHaveBeenCalledTimes(),
spy = new jasmineUnderTest.Spy('sample-spy');
spy();
spy();
matcher.compare(spy, 2);
expect(spy.calls.count()).toBe(2);
expect(spy.calls.unverifiedCount()).toBe(0);
});
});

View File

@@ -92,4 +92,19 @@ describe('toHaveBeenCalledWith', function() {
matcher.compare(fn);
}).toThrowError(/Expected a spy, but got Function./);
});
it('set the correct calls as verified when passing', function() {
const matchersUtil = {
contains: jasmine.createSpy('interaction-check').and.returnValue(true),
pp: jasmineUnderTest.makePrettyPrinter()
},
matcher = jasmineUnderTest.matchers.toHaveBeenCalledWith(matchersUtil),
calledSpy = new jasmineUnderTest.Spy('called-spy');
calledSpy('a', 'b');
matcher.compare(calledSpy, 'a', 'b');
expect(calledSpy.calls.count()).toBe(1);
expect(calledSpy.calls.unverifiedCount()).toBe(0);
});
});

View File

@@ -0,0 +1,137 @@
describe('toHaveNoOtherSpyInteractions', function() {
it('passes when there are no spy interactions', function() {
let matcher = jasmineUnderTest.matchers.toHaveNoOtherSpyInteractions();
let spyObj = jasmineUnderTest
.getEnv()
.createSpyObj('NewClass', ['spyA', 'spyB']);
let result = matcher.compare(spyObj);
expect(result.pass).toBeTrue();
});
it('passes when there are multiple spy interactions where checked by toHaveBeenCalled', function() {
let matcher = jasmineUnderTest.matchers.toHaveNoOtherSpyInteractions();
let toHaveBeenCalledMatcher = jasmineUnderTest.matchers.toHaveBeenCalled();
let spyObj = jasmineUnderTest
.getEnv()
.createSpyObj('NewClass', ['spyA', 'spyB']);
spyObj.spyA();
spyObj.spyB();
spyObj.spyA();
toHaveBeenCalledMatcher.compare(spyObj.spyA);
toHaveBeenCalledMatcher.compare(spyObj.spyB);
let result = matcher.compare(spyObj);
expect(result.pass).toBeTrue();
});
it('fails when there are spy interactions', function() {
let matcher = jasmineUnderTest.matchers.toHaveNoOtherSpyInteractions();
let spyObj = jasmineUnderTest
.getEnv()
.createSpyObj('NewClass', ['spyA', 'spyB']);
spyObj.spyA();
let result = matcher.compare(spyObj);
expect(result.pass).toBeFalse();
expect(result.message).toContain(
"Unverified spies' calls have been found in:"
);
});
it('shows the right message is negated', function() {
let matcher = jasmineUnderTest.matchers.toHaveNoOtherSpyInteractions();
let spyObj = jasmineUnderTest
.getEnv()
.createSpyObj('NewClass', ['spyA', 'spyB']);
spyObj.spyA();
spyObj.spyB();
let result = matcher.compare(spyObj);
expect(result.pass).toBeFalse(),
expect(result.message).toContain(
"Unverified spies' calls have been found in:"
);
});
it('passes when only non-observed spy object interactions are interacted', function() {
let matcher = jasmineUnderTest.matchers.toHaveNoOtherSpyInteractions();
let spyObj = jasmineUnderTest
.getEnv()
.createSpyObj('NewClass', ['spyA', 'spyB']);
spyObj.otherMethod = function() {};
spyObj.otherMethod();
let result = matcher.compare(spyObj);
expect(result.pass).toBeTrue();
expect(result.message).toContain("Spies' calls are all verified.");
});
it(`throws an error if a non-object is passed`, function() {
let matcher = jasmineUnderTest.matchers.toHaveNoOtherSpyInteractions();
expect(function() {
matcher.compare(true);
}).toThrowError(Error, /Expected an object, but got/);
expect(function() {
matcher.compare(123);
}).toThrowError(Error, /Expected an object, but got/);
expect(function() {
matcher.compare('string');
}).toThrowError(Error, /Expected an object, but got/);
});
it('throws an error if arguments are passed', function() {
let matcher = jasmineUnderTest.matchers.toHaveNoOtherSpyInteractions();
let spyObj = jasmineUnderTest
.getEnv()
.createSpyObj('NewClass', ['spyA', 'spyB']);
expect(function() {
matcher.compare(spyObj, 'an argument');
}).toThrowError(Error, /Does not take arguments/);
});
it('throws an error if the spy object has no spies', function() {
let matcher = jasmineUnderTest.matchers.toHaveNoOtherSpyInteractions();
const spyObj = jasmineUnderTest
.getEnv()
.createSpyObj('NewClass', ['notSpy']);
// Removing spy since spy objects cannot be created without spies.
spyObj.notSpy = function() {};
expect(function() {
matcher.compare(spyObj);
}).toThrowError(
Error,
/Expected an object with spies, but object has no spies/
);
});
it('handles multiple interactions with a single spy', function() {
const matchersUtil = new jasmineUnderTest.MatchersUtil({
pp: jasmineUnderTest.makePrettyPrinter()
}),
matcher = jasmineUnderTest.matchers.toHaveNoOtherSpyInteractions(),
toHaveBeenCalledWithMatcher = jasmineUnderTest.matchers.toHaveBeenCalledWith(
matchersUtil
),
spyObj = jasmineUnderTest
.getEnv()
.createSpyObj('NewClass', ['spyA', 'spyB']);
spyObj.spyA('x');
spyObj.spyA('y');
toHaveBeenCalledWithMatcher.compare(spyObj.spyA, 'x');
let result = matcher.compare(spyObj);
expect(result.pass).toBeFalse();
});
});

View File

@@ -125,6 +125,16 @@ getJasmineRequireObj().CallTracker = function(j$) {
this.saveArgumentsByValue = function() {
opts.cloneArgs = true;
};
/**
* Get the number of unverified invocations of this spy.
* @name Spy#calls#unverifiedCount
* @function
* @return {Integer}
*/
this.unverifiedCount = function() {
return calls.reduce((count, call) => count + (call.verified ? 0 : 1), 0);
};
}
return CallTracker;

View File

@@ -26,7 +26,8 @@ getJasmineRequireObj().Spy = function(j$) {
const callData = {
object: context,
invocationOrder: nextOrder(),
args: Array.prototype.slice.apply(args)
args: Array.prototype.slice.apply(args),
verified: false
};
callTracker.track(callData);

View File

@@ -30,6 +30,7 @@ getJasmineRequireObj().requireMatchers = function(jRequire, j$) {
'toHaveClass',
'toHaveClasses',
'toHaveSpyInteractions',
'toHaveNoOtherSpyInteractions',
'toMatch',
'toThrow',
'toThrowError',

View File

@@ -34,6 +34,8 @@ getJasmineRequireObj().toHaveBeenCalled = function(j$) {
result.pass = actual.calls.any();
actual.calls.all().forEach(call => (call.verified = true));
result.message = result.pass
? 'Expected spy ' + actual.and.identity + ' not to have been called.'
: 'Expected spy ' + actual.and.identity + ' to have been called.';

View File

@@ -50,6 +50,9 @@ getJasmineRequireObj().toHaveBeenCalledBefore = function(j$) {
result.pass = latest1stSpyCall < first2ndSpyCall;
if (result.pass) {
firstSpy.calls.mostRecent().verified = true;
latterSpy.calls.first().verified = true;
result.message =
'Expected spy ' +
firstSpy.and.identity +

View File

@@ -13,7 +13,7 @@ getJasmineRequireObj().toHaveBeenCalledOnceWith = function(j$) {
* @example
* expect(mySpy).toHaveBeenCalledOnceWith('foo', 'bar', 2);
*/
function toHaveBeenCalledOnceWith(util) {
function toHaveBeenCalledOnceWith(matchersUtil) {
return {
compare: function() {
const args = Array.prototype.slice.call(arguments, 0),
@@ -22,20 +22,29 @@ getJasmineRequireObj().toHaveBeenCalledOnceWith = function(j$) {
if (!j$.isSpy(actual)) {
throw new Error(
getErrorMsg('Expected a spy, but got ' + util.pp(actual) + '.')
getErrorMsg(
'Expected a spy, but got ' + matchersUtil.pp(actual) + '.'
)
);
}
const prettyPrintedCalls = actual.calls
.allArgs()
.map(function(argsForCall) {
return ' ' + util.pp(argsForCall);
return ' ' + matchersUtil.pp(argsForCall);
});
if (
actual.calls.count() === 1 &&
util.contains(actual.calls.allArgs(), expectedArgs)
matchersUtil.contains(actual.calls.allArgs(), expectedArgs)
) {
const firstIndex = actual.calls
.all()
.findIndex(call => matchersUtil.equals(call.args, expectedArgs));
if (firstIndex > -1) {
actual.calls.all()[firstIndex].verified = true;
}
return {
pass: true,
message:
@@ -43,7 +52,7 @@ getJasmineRequireObj().toHaveBeenCalledOnceWith = function(j$) {
actual.and.identity +
' to have been called 0 times, multiple times, or once, but with arguments different from:\n' +
' ' +
util.pp(expectedArgs) +
matchersUtil.pp(expectedArgs) +
'\n' +
'But the actual call was:\n' +
prettyPrintedCalls.join(',\n') +
@@ -54,7 +63,7 @@ getJasmineRequireObj().toHaveBeenCalledOnceWith = function(j$) {
function getDiffs() {
return actual.calls.allArgs().map(function(argsForCall, callIx) {
const diffBuilder = new j$.DiffBuilder();
util.equals(argsForCall, expectedArgs, diffBuilder);
matchersUtil.equals(argsForCall, expectedArgs, diffBuilder);
return diffBuilder.getMessage();
});
}
@@ -87,7 +96,7 @@ getJasmineRequireObj().toHaveBeenCalledOnceWith = function(j$) {
actual.and.identity +
' to have been called only once, and with given args:\n' +
' ' +
util.pp(expectedArgs) +
matchersUtil.pp(expectedArgs) +
'\n' +
butString()
};

View File

@@ -36,23 +36,35 @@ getJasmineRequireObj().toHaveBeenCalledTimes = function(j$) {
}
actual = args[0];
const calls = actual.calls.count();
const callsCount = actual.calls.count();
const timesMessage = expected === 1 ? 'once' : expected + ' times';
result.pass = calls === expected;
result.pass = callsCount === expected;
if (result.pass) {
const allCalls = actual.calls.all();
const max = Math.min(expected, callsCount);
for (let i = 0; i < max; i++) {
allCalls[i].verified = true;
}
}
result.message = result.pass
? 'Expected spy ' +
actual.and.identity +
' not to have been called ' +
timesMessage +
'. It was called ' +
calls +
callsCount +
' times.'
: 'Expected spy ' +
actual.and.identity +
' to have been called ' +
timesMessage +
'. It was called ' +
calls +
callsCount +
' times.';
return result;
}

View File

@@ -44,6 +44,11 @@ getJasmineRequireObj().toHaveBeenCalledWith = function(j$) {
}
if (matchersUtil.contains(actual.calls.allArgs(), expectedArgs)) {
actual.calls
.all()
.filter(call => matchersUtil.equals(call.args, expectedArgs))
.forEach(call => (call.verified = true));
result.pass = true;
result.message = function() {
return (

View File

@@ -0,0 +1,75 @@
getJasmineRequireObj().toHaveNoOtherSpyInteractions = function(j$) {
const getErrorMsg = j$.formatErrorMsg(
'<toHaveNoOtherSpyInteractions>',
'expect(<spyObj>).toHaveNoOtherSpyInteractions()'
);
/**
* {@link expect} the actual (a {@link SpyObj}) spies to have not been called except interactions which was already tracked with `toHaveBeenCalled`.
* @function
* @name matchers#toHaveNoOtherSpyInteractions
* @example
* expect(mySpyObj).toHaveNoOtherSpyInteractions();
* expect(mySpyObj).not.toHaveNoOtherSpyInteractions();
*/
function toHaveNoOtherSpyInteractions(matchersUtil) {
return {
compare: function(actual) {
const result = {};
if (!j$.isObject_(actual)) {
throw new Error(
getErrorMsg('Expected an object, but got ' + typeof actual + '.')
);
}
if (arguments.length > 1) {
throw new Error(getErrorMsg('Does not take arguments'));
}
result.pass = true;
let hasSpy = false;
const unexpectedCallsIn = [];
for (const spy of Object.values(actual)) {
if (!j$.isSpy(spy)) {
continue;
}
hasSpy = true;
if (!spy.calls.all().every(call => call.verified)) {
unexpectedCallsIn.push([
spy.and.identity,
spy.calls.unverifiedCount()
]);
result.pass = false;
}
}
if (!hasSpy) {
throw new Error(
getErrorMsg(
'Expected an object with spies, but object has no spies.'
)
);
}
result.message = result.pass
? "Spies' calls are all verified."
: "Unverified spies' calls have been found in: " +
unexpectedCallsIn
.map(
([spyName, unverifiedCount]) =>
`${spyName} (${unverifiedCount} unverified call(s))`
)
.join(', ');
return result;
}
};
}
return toHaveNoOtherSpyInteractions;
};