sinon
Version:
JavaScript test spies, stubs and mocks.
322 lines (252 loc) • 8.51 kB
JavaScript
const arrayProto = require("@sinonjs/commons").prototypes.array;
const proxyInvoke = require("./proxy-invoke");
const proxyCallToString = require("./proxy-call").toString;
const timesInWords = require("./util/core/times-in-words");
const extend = require("./util/core/extend");
const match = require("@sinonjs/samsam").createMatcher;
const stub = require("./stub");
const assert = require("./assert");
const deepEqual = require("@sinonjs/samsam").deepEqual;
const inspect = require("util").inspect;
const valueToString = require("@sinonjs/commons").valueToString;
const every = arrayProto.every;
const forEach = arrayProto.forEach;
const push = arrayProto.push;
const slice = arrayProto.slice;
function callCountInWords(callCount) {
if (callCount === 0) {
return "never called";
}
return `called ${timesInWords(callCount)}`;
}
function expectedCallCountInWords(expectation) {
const min = expectation.minCalls;
const max = expectation.maxCalls;
if (typeof min === "number" && typeof max === "number") {
let str = timesInWords(min);
if (min !== max) {
str = `at least ${str} and at most ${timesInWords(max)}`;
}
return str;
}
if (typeof min === "number") {
return `at least ${timesInWords(min)}`;
}
return `at most ${timesInWords(max)}`;
}
function receivedMinCalls(expectation) {
const hasMinLimit = typeof expectation.minCalls === "number";
return !hasMinLimit || expectation.callCount >= expectation.minCalls;
}
function receivedMaxCalls(expectation) {
if (typeof expectation.maxCalls !== "number") {
return false;
}
return expectation.callCount === expectation.maxCalls;
}
function verifyMatcher(possibleMatcher, arg) {
const isMatcher = match.isMatcher(possibleMatcher);
return (isMatcher && possibleMatcher.test(arg)) || true;
}
const mockExpectation = {
minCalls: 1,
maxCalls: 1,
create: function create(methodName) {
const expectation = extend.nonEnum(stub(), mockExpectation);
delete expectation.create;
expectation.method = methodName;
return expectation;
},
invoke: function invoke(func, thisValue, args) {
this.verifyCallAllowed(thisValue, args);
return proxyInvoke.apply(this, arguments);
},
atLeast: function atLeast(num) {
if (typeof num !== "number") {
throw new TypeError(`'${valueToString(num)}' is not number`);
}
if (!this.limitsSet) {
this.maxCalls = null;
this.limitsSet = true;
}
this.minCalls = num;
return this;
},
atMost: function atMost(num) {
if (typeof num !== "number") {
throw new TypeError(`'${valueToString(num)}' is not number`);
}
if (!this.limitsSet) {
this.minCalls = null;
this.limitsSet = true;
}
this.maxCalls = num;
return this;
},
never: function never() {
return this.exactly(0);
},
once: function once() {
return this.exactly(1);
},
twice: function twice() {
return this.exactly(2);
},
thrice: function thrice() {
return this.exactly(3);
},
exactly: function exactly(num) {
if (typeof num !== "number") {
throw new TypeError(`'${valueToString(num)}' is not a number`);
}
this.atLeast(num);
return this.atMost(num);
},
met: function met() {
return !this.failed && receivedMinCalls(this);
},
verifyCallAllowed: function verifyCallAllowed(thisValue, args) {
const expectedArguments = this.expectedArguments;
if (receivedMaxCalls(this)) {
this.failed = true;
mockExpectation.fail(
`${this.method} already called ${timesInWords(this.maxCalls)}`,
);
}
if ("expectedThis" in this && this.expectedThis !== thisValue) {
mockExpectation.fail(
`${this.method} called with ${valueToString(
thisValue,
)} as thisValue, expected ${valueToString(this.expectedThis)}`,
);
}
if (!("expectedArguments" in this)) {
return;
}
if (!args) {
mockExpectation.fail(
`${this.method} received no arguments, expected ${inspect(
expectedArguments,
)}`,
);
}
if (args.length < expectedArguments.length) {
mockExpectation.fail(
`${this.method} received too few arguments (${inspect(
args,
)}), expected ${inspect(expectedArguments)}`,
);
}
if (
this.expectsExactArgCount &&
args.length !== expectedArguments.length
) {
mockExpectation.fail(
`${this.method} received too many arguments (${inspect(
args,
)}), expected ${inspect(expectedArguments)}`,
);
}
forEach(
expectedArguments,
function (expectedArgument, i) {
if (!verifyMatcher(expectedArgument, args[i])) {
mockExpectation.fail(
`${this.method} received wrong arguments ${inspect(
args,
)}, didn't match ${String(expectedArguments)}`,
);
}
if (!deepEqual(args[i], expectedArgument)) {
mockExpectation.fail(
`${this.method} received wrong arguments ${inspect(
args,
)}, expected ${inspect(expectedArguments)}`,
);
}
},
this,
);
},
allowsCall: function allowsCall(thisValue, args) {
const expectedArguments = this.expectedArguments;
if (this.met() && receivedMaxCalls(this)) {
return false;
}
if ("expectedThis" in this && this.expectedThis !== thisValue) {
return false;
}
if (!("expectedArguments" in this)) {
return true;
}
// eslint-disable-next-line no-underscore-dangle
const _args = args || [];
if (_args.length < expectedArguments.length) {
return false;
}
if (
this.expectsExactArgCount &&
_args.length !== expectedArguments.length
) {
return false;
}
return every(expectedArguments, function (expectedArgument, i) {
if (!verifyMatcher(expectedArgument, _args[i])) {
return false;
}
if (!deepEqual(_args[i], expectedArgument)) {
return false;
}
return true;
});
},
withArgs: function withArgs() {
this.expectedArguments = slice(arguments);
return this;
},
withExactArgs: function withExactArgs() {
this.withArgs.apply(this, arguments);
this.expectsExactArgCount = true;
return this;
},
on: function on(thisValue) {
this.expectedThis = thisValue;
return this;
},
toString: function () {
const args = slice(this.expectedArguments || []);
if (!this.expectsExactArgCount) {
push(args, "[...]");
}
const callStr = proxyCallToString.call({
proxy: this.method || "anonymous mock expectation",
args: args,
});
const message = `${callStr.replace(
", [...",
"[, ...",
)} ${expectedCallCountInWords(this)}`;
if (this.met()) {
return `Expectation met: ${message}`;
}
return `Expected ${message} (${callCountInWords(this.callCount)})`;
},
verify: function verify() {
if (!this.met()) {
mockExpectation.fail(String(this));
} else {
mockExpectation.pass(String(this));
}
return true;
},
pass: function pass(message) {
assert.pass(message);
},
fail: function fail(message) {
const exception = new Error(message);
exception.name = "ExpectationError";
throw exception;
},
};
module.exports = mockExpectation;
;