Squashed spy refactor and new spy syntax
Jasmine spies now have a 'and' property which allows the user to change the spy's execution strategy-- such as '.and.callReturn(4)' and a 'calls' property which allows inspection of the calls a spy has received. * This is a breaking change * There is a CallTracker that keeps track of all calls and arguments and a SpyStrategy which determines what the spy should do when it is called.
This commit is contained in:
committed by
Colin O'Byrne and JR Boyens
parent
18c30566bd
commit
3847557bbc
50
src/core/CallTracker.js
Normal file
50
src/core/CallTracker.js
Normal file
@@ -0,0 +1,50 @@
|
||||
getJasmineRequireObj().CallTracker = function() {
|
||||
|
||||
function CallTracker() {
|
||||
var calls = [];
|
||||
|
||||
this.track = function(context) {
|
||||
calls.push(context);
|
||||
};
|
||||
|
||||
this.any = function() {
|
||||
return !!calls.length;
|
||||
};
|
||||
|
||||
this.count = function() {
|
||||
return calls.length;
|
||||
};
|
||||
|
||||
this.argsFor = function(index) {
|
||||
var call = calls[index];
|
||||
return call ? call.args : [];
|
||||
};
|
||||
|
||||
this.all = function() {
|
||||
return calls;
|
||||
};
|
||||
|
||||
this.allArgs = function() {
|
||||
var callArgs = [];
|
||||
for(var i = 0; i < calls.length; i++){
|
||||
callArgs.push(calls[i].args);
|
||||
}
|
||||
|
||||
return callArgs;
|
||||
};
|
||||
|
||||
this.first = function() {
|
||||
return calls[0];
|
||||
};
|
||||
|
||||
this.mostRecent = function() {
|
||||
return calls[calls.length - 1];
|
||||
};
|
||||
|
||||
this.reset = function() {
|
||||
calls = [];
|
||||
};
|
||||
}
|
||||
|
||||
return CallTracker;
|
||||
};
|
||||
@@ -1,6 +1,7 @@
|
||||
getJasmineRequireObj().Env = function(j$) {
|
||||
function Env(options) {
|
||||
options = options || {};
|
||||
|
||||
var self = this;
|
||||
var global = options.global || j$.getGlobal();
|
||||
|
||||
@@ -9,7 +10,8 @@ getJasmineRequireObj().Env = function(j$) {
|
||||
var realSetTimeout = j$.getGlobal().setTimeout;
|
||||
this.clock = new j$.Clock(global, new j$.DelayedFunctionScheduler());
|
||||
|
||||
this.spies_ = [];
|
||||
var spies = [];
|
||||
|
||||
this.currentSpec = null;
|
||||
|
||||
this.reporter = new j$.ReportDispatcher([
|
||||
@@ -83,13 +85,13 @@ getJasmineRequireObj().Env = function(j$) {
|
||||
|
||||
// TODO: we may just be able to pass in the fn instead of wrapping here
|
||||
var buildExpectationResult = j$.buildExpectationResult,
|
||||
exceptionFormatter = new j$.ExceptionFormatter(),
|
||||
expectationResultFactory = function(attrs) {
|
||||
attrs.messageFormatter = exceptionFormatter.message;
|
||||
attrs.stackFormatter = exceptionFormatter.stack;
|
||||
exceptionFormatter = new j$.ExceptionFormatter(),
|
||||
expectationResultFactory = function(attrs) {
|
||||
attrs.messageFormatter = exceptionFormatter.message;
|
||||
attrs.stackFormatter = exceptionFormatter.stack;
|
||||
|
||||
return buildExpectationResult(attrs);
|
||||
};
|
||||
return buildExpectationResult(attrs);
|
||||
};
|
||||
|
||||
// TODO: fix this naming, and here's where the value comes in
|
||||
this.catchExceptions = function(value) {
|
||||
@@ -101,7 +103,7 @@ getJasmineRequireObj().Env = function(j$) {
|
||||
return catchExceptions;
|
||||
};
|
||||
|
||||
this.catchException = function(e){
|
||||
this.catchException = function(e) {
|
||||
return j$.Spec.isPendingSpecException(e) || catchExceptions;
|
||||
};
|
||||
|
||||
@@ -152,8 +154,16 @@ getJasmineRequireObj().Env = function(j$) {
|
||||
|
||||
return spec;
|
||||
|
||||
function removeAllSpies() {
|
||||
for (var i = 0; i < spies.length; i++) {
|
||||
var spyEntry = spies[i];
|
||||
spyEntry.baseObj[spyEntry.methodName] = spyEntry.originalValue;
|
||||
}
|
||||
spies = [];
|
||||
}
|
||||
|
||||
function specResultCallback(result) {
|
||||
self.removeAllSpies();
|
||||
removeAllSpies();
|
||||
j$.Expectation.resetMatchers();
|
||||
customEqualityTesters.length = 0;
|
||||
self.clock.uninstall();
|
||||
@@ -198,6 +208,34 @@ getJasmineRequireObj().Env = function(j$) {
|
||||
});
|
||||
this.topSuite.execute(self.reporter.jasmineDone);
|
||||
};
|
||||
|
||||
this.spyOn = function(obj, methodName) {
|
||||
if (j$.util.isUndefined(obj)) {
|
||||
throw "spyOn could not find an object to spy upon for " + methodName + "()";
|
||||
}
|
||||
|
||||
if (j$.util.isUndefined(obj[methodName])) {
|
||||
throw methodName + '() method does not exist';
|
||||
}
|
||||
|
||||
if (obj[methodName] && j$.isSpy(obj[methodName])) {
|
||||
//TODO?: should this return the current spy? Downside: may cause user confusion about spy state
|
||||
throw methodName + ' has already been spied upon';
|
||||
}
|
||||
|
||||
var spy = j$.createSpy(methodName, obj[methodName]);
|
||||
|
||||
spies.push({
|
||||
spy: spy,
|
||||
baseObj: obj,
|
||||
methodName: methodName,
|
||||
originalValue: obj[methodName]
|
||||
});
|
||||
|
||||
obj[methodName] = spy;
|
||||
|
||||
return spy;
|
||||
};
|
||||
}
|
||||
|
||||
Env.prototype.addMatchers = function(matchersToAdd) {
|
||||
@@ -212,40 +250,6 @@ getJasmineRequireObj().Env = function(j$) {
|
||||
return this.currentSpec.expect(actual);
|
||||
};
|
||||
|
||||
Env.prototype.spyOn = function(obj, methodName) {
|
||||
if (j$.util.isUndefined(obj)) {
|
||||
throw "spyOn could not find an object to spy upon for " + methodName + "()";
|
||||
}
|
||||
|
||||
if (j$.util.isUndefined(obj[methodName])) {
|
||||
throw methodName + '() method does not exist';
|
||||
}
|
||||
|
||||
if (obj[methodName] && obj[methodName].isSpy) {
|
||||
//TODO?: should this return the current spy? Downside: may cause user confusion about spy state
|
||||
throw new Error(methodName + ' has already been spied upon');
|
||||
}
|
||||
|
||||
var spyObj = j$.createSpy(methodName);
|
||||
|
||||
this.spies_.push(spyObj);
|
||||
spyObj.baseObj = obj;
|
||||
spyObj.methodName = methodName;
|
||||
spyObj.originalValue = obj[methodName];
|
||||
|
||||
obj[methodName] = spyObj;
|
||||
|
||||
return spyObj;
|
||||
};
|
||||
|
||||
// TODO: move this to closure
|
||||
Env.prototype.removeAllSpies = function() {
|
||||
for (var i = 0; i < this.spies_.length; i++) {
|
||||
var spy = this.spies_[i];
|
||||
spy.baseObj[spy.methodName] = spy.originalValue;
|
||||
}
|
||||
this.spies_ = [];
|
||||
};
|
||||
|
||||
// TODO: move this to closure
|
||||
Env.prototype.versionString = function() {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
getJasmineRequireObj().StringPrettyPrinter = function(j$) {
|
||||
getJasmineRequireObj().pp = function(j$) {
|
||||
|
||||
function PrettyPrinter() {
|
||||
this.ppNestLevel_ = 0;
|
||||
@@ -18,7 +18,7 @@ getJasmineRequireObj().StringPrettyPrinter = function(j$) {
|
||||
} else if (typeof value === 'string') {
|
||||
this.emitString(value);
|
||||
} else if (j$.isSpy(value)) {
|
||||
this.emitScalar("spy on " + value.identity);
|
||||
this.emitScalar("spy on " + value.and.identity());
|
||||
} else if (value instanceof RegExp) {
|
||||
this.emitScalar(value.toString());
|
||||
} else if (typeof value === 'function') {
|
||||
@@ -50,7 +50,7 @@ getJasmineRequireObj().StringPrettyPrinter = function(j$) {
|
||||
if (!obj.hasOwnProperty(property)) continue;
|
||||
if (property == '__Jasmine_been_here_before__') continue;
|
||||
fn(property, obj.__lookupGetter__ ? (!j$.util.isUndefined(obj.__lookupGetter__(property)) &&
|
||||
obj.__lookupGetter__(property) !== null) : false);
|
||||
obj.__lookupGetter__(property) !== null) : false);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -64,6 +64,7 @@ getJasmineRequireObj().StringPrettyPrinter = function(j$) {
|
||||
|
||||
this.string = '';
|
||||
}
|
||||
|
||||
j$.util.inherit(StringPrettyPrinter, PrettyPrinter);
|
||||
|
||||
StringPrettyPrinter.prototype.emitScalar = function(value) {
|
||||
@@ -123,5 +124,9 @@ getJasmineRequireObj().StringPrettyPrinter = function(j$) {
|
||||
this.string += value;
|
||||
};
|
||||
|
||||
return StringPrettyPrinter;
|
||||
return function(value) {
|
||||
var stringPrettyPrinter = new StringPrettyPrinter();
|
||||
stringPrettyPrinter.format(value);
|
||||
return stringPrettyPrinter.string;
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,86 +0,0 @@
|
||||
getJasmineRequireObj().Spy = function(j$) {
|
||||
|
||||
function Spy(name) {
|
||||
this.identity = name || 'unknown';
|
||||
this.isSpy = true;
|
||||
this.plan = function() {
|
||||
};
|
||||
this.mostRecentCall = {};
|
||||
|
||||
this.argsForCall = [];
|
||||
this.calls = [];
|
||||
}
|
||||
|
||||
Spy.prototype.andCallThrough = function() {
|
||||
this.plan = this.originalValue;
|
||||
return this;
|
||||
};
|
||||
|
||||
Spy.prototype.andReturn = function(value) {
|
||||
this.plan = function() {
|
||||
return value;
|
||||
};
|
||||
return this;
|
||||
};
|
||||
|
||||
Spy.prototype.andThrow = function(exceptionMsg) {
|
||||
this.plan = function() {
|
||||
throw exceptionMsg;
|
||||
};
|
||||
return this;
|
||||
};
|
||||
|
||||
Spy.prototype.andCallFake = function(fakeFunc) {
|
||||
this.plan = fakeFunc;
|
||||
return this;
|
||||
};
|
||||
|
||||
Spy.prototype.reset = function() {
|
||||
this.wasCalled = false;
|
||||
this.callCount = 0;
|
||||
this.argsForCall = [];
|
||||
this.calls = [];
|
||||
this.mostRecentCall = {};
|
||||
};
|
||||
|
||||
j$.createSpy = function(name) {
|
||||
|
||||
var spyObj = function() {
|
||||
spyObj.wasCalled = true;
|
||||
spyObj.callCount++;
|
||||
var args = j$.util.argsToArray(arguments);
|
||||
spyObj.mostRecentCall.object = this;
|
||||
spyObj.mostRecentCall.args = args;
|
||||
spyObj.argsForCall.push(args);
|
||||
spyObj.calls.push({object: this, args: args});
|
||||
return spyObj.plan.apply(this, arguments);
|
||||
};
|
||||
|
||||
var spy = new Spy(name);
|
||||
|
||||
for (var prop in spy) {
|
||||
spyObj[prop] = spy[prop];
|
||||
}
|
||||
|
||||
spyObj.reset();
|
||||
|
||||
return spyObj;
|
||||
};
|
||||
|
||||
j$.isSpy = function(putativeSpy) {
|
||||
return putativeSpy && putativeSpy.isSpy;
|
||||
};
|
||||
|
||||
j$.createSpyObj = function(baseName, methodNames) {
|
||||
if (!j$.isArray_(methodNames) || methodNames.length === 0) {
|
||||
throw "createSpyObj requires a non-empty array of method names to create spies for";
|
||||
}
|
||||
var obj = {};
|
||||
for (var i = 0; i < methodNames.length; i++) {
|
||||
obj[methodNames[i]] = j$.createSpy(baseName + '.' + methodNames[i]);
|
||||
}
|
||||
return obj;
|
||||
};
|
||||
|
||||
return Spy;
|
||||
};
|
||||
45
src/core/SpyStrategy.js
Normal file
45
src/core/SpyStrategy.js
Normal file
@@ -0,0 +1,45 @@
|
||||
getJasmineRequireObj().SpyStrategy = function() {
|
||||
|
||||
function SpyStrategy(options) {
|
||||
options = options || {};
|
||||
|
||||
var identity = options.name || "unknown",
|
||||
originalFn = options.fn || function() {},
|
||||
getSpy = options.getSpy || function() {},
|
||||
plan = function() {};
|
||||
|
||||
this.identity = function() {
|
||||
return identity;
|
||||
};
|
||||
|
||||
this.exec = function() {
|
||||
return plan.apply(this, arguments);
|
||||
};
|
||||
|
||||
this.callThrough = function() {
|
||||
plan = originalFn;
|
||||
return getSpy();
|
||||
};
|
||||
|
||||
this.callReturn = function(value) {
|
||||
plan = function() {
|
||||
return value;
|
||||
};
|
||||
return getSpy();
|
||||
};
|
||||
|
||||
this.callThrow = function(something) {
|
||||
plan = function() {
|
||||
throw something;
|
||||
};
|
||||
return getSpy();
|
||||
};
|
||||
|
||||
this.callFake = function(fn) {
|
||||
plan = fn;
|
||||
return getSpy();
|
||||
};
|
||||
}
|
||||
|
||||
return SpyStrategy;
|
||||
};
|
||||
@@ -37,12 +37,6 @@ getJasmineRequireObj().base = function(j$) {
|
||||
return Object.prototype.toString.apply(value) === '[object ' + typeName + ']';
|
||||
};
|
||||
|
||||
j$.pp = function(value) {
|
||||
var stringPrettyPrinter = new j$.StringPrettyPrinter();
|
||||
stringPrettyPrinter.format(value);
|
||||
return stringPrettyPrinter.string;
|
||||
};
|
||||
|
||||
j$.isDomNode = function(obj) {
|
||||
return obj.nodeType > 0;
|
||||
};
|
||||
@@ -54,4 +48,45 @@ getJasmineRequireObj().base = function(j$) {
|
||||
j$.objectContaining = function(sample) {
|
||||
return new j$.ObjectContaining(sample);
|
||||
};
|
||||
};
|
||||
|
||||
j$.createSpy = function(name, originalFn) {
|
||||
|
||||
var spyStrategy = new j$.SpyStrategy({
|
||||
name: name,
|
||||
fn: originalFn,
|
||||
getSpy: function() { return spy; }
|
||||
}),
|
||||
callTracker = new j$.CallTracker(),
|
||||
spy = function() {
|
||||
callTracker.track({
|
||||
object: this,
|
||||
args: Array.prototype.slice.apply(arguments)
|
||||
});
|
||||
return spyStrategy.exec.apply(this, arguments);
|
||||
};
|
||||
|
||||
spy.and = spyStrategy;
|
||||
spy.calls = callTracker;
|
||||
|
||||
return spy;
|
||||
};
|
||||
|
||||
j$.isSpy = function(putativeSpy) {
|
||||
if (!putativeSpy) {
|
||||
return false;
|
||||
}
|
||||
return putativeSpy.and instanceof j$.SpyStrategy &&
|
||||
putativeSpy.calls instanceof j$.CallTracker;
|
||||
};
|
||||
|
||||
j$.createSpyObj = function(baseName, methodNames) {
|
||||
if (!j$.isArray_(methodNames) || methodNames.length === 0) {
|
||||
throw "createSpyObj requires a non-empty array of method names to create spies for";
|
||||
}
|
||||
var obj = {};
|
||||
for (var i = 0; i < methodNames.length; i++) {
|
||||
obj[methodNames[i]] = j$.createSpy(baseName + '.' + methodNames[i]);
|
||||
}
|
||||
return obj;
|
||||
};
|
||||
};
|
||||
|
||||
@@ -13,11 +13,11 @@ getJasmineRequireObj().toHaveBeenCalled = function(j$) {
|
||||
throw new Error('toHaveBeenCalled does not take arguments, use toHaveBeenCalledWith');
|
||||
}
|
||||
|
||||
result.pass = actual.wasCalled;
|
||||
result.pass = actual.calls.any();
|
||||
|
||||
result.message = result.pass ?
|
||||
"Expected spy " + actual.identity + " not to have been called." :
|
||||
"Expected spy " + actual.identity + " to have been called.";
|
||||
"Expected spy " + actual.and.identity() + " not to have been called." :
|
||||
"Expected spy " + actual.and.identity() + " to have been called.";
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -12,13 +12,13 @@ getJasmineRequireObj().toHaveBeenCalledWith = function(j$) {
|
||||
}
|
||||
|
||||
return {
|
||||
pass: util.contains(actual.argsForCall, expectedArgs)
|
||||
pass: util.contains(actual.calls.allArgs(), expectedArgs)
|
||||
};
|
||||
},
|
||||
message: function(actual) {
|
||||
return {
|
||||
affirmative: "Expected spy " + actual.identity + " to have been called.",
|
||||
negative: "Expected spy " + actual.identity + " not to have been called."
|
||||
affirmative: "Expected spy " + actual.and.identity() + " to have been called.",
|
||||
negative: "Expected spy " + actual.and.identity() + " not to have been called."
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
@@ -13,6 +13,7 @@ getJasmineRequireObj().core = function(jRequire) {
|
||||
jRequire.base(j$);
|
||||
j$.util = jRequire.util();
|
||||
j$.Any = jRequire.Any();
|
||||
j$.CallTracker = jRequire.CallTracker();
|
||||
j$.Clock = jRequire.Clock();
|
||||
j$.DelayedFunctionScheduler = jRequire.DelayedFunctionScheduler();
|
||||
j$.Env = jRequire.Env(j$);
|
||||
@@ -22,11 +23,11 @@ getJasmineRequireObj().core = function(jRequire) {
|
||||
j$.JsApiReporter = jRequire.JsApiReporter();
|
||||
j$.matchersUtil = jRequire.matchersUtil(j$);
|
||||
j$.ObjectContaining = jRequire.ObjectContaining(j$);
|
||||
j$.StringPrettyPrinter = jRequire.StringPrettyPrinter(j$);
|
||||
j$.pp = jRequire.pp(j$);
|
||||
j$.QueueRunner = jRequire.QueueRunner();
|
||||
j$.ReportDispatcher = jRequire.ReportDispatcher();
|
||||
j$.Spec = jRequire.Spec();
|
||||
j$.Spy = jRequire.Spy(j$);
|
||||
j$.SpyStrategy = jRequire.SpyStrategy();
|
||||
j$.Suite = jRequire.Suite();
|
||||
j$.Timer = jRequire.Timer();
|
||||
j$.version = jRequire.version();
|
||||
|
||||
Reference in New Issue
Block a user