Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Browse files

Updated the Smoke mocking library.

  - based on karnowski/smoke from today (2009-07-17)
  - includes Andy Kent's feature for mocking anonymous functions
  - includes Kris Chambers' many many refactorings and bug fixes
  - includes Kris Chambers' feature of throwing an argument
    mismatch error when an expectation is not matched
  - includes my fix for properly tearing down mocks on reset
  • Loading branch information...
commit b87569ecef75eb1de7d6873d08a1a705b6df2ea5 1 parent 8223105
Larry Karnowski authored
View
4 lib/screw.mocking.js
@@ -5,6 +5,10 @@ Screw.Matchers.mock = function(m) {
return Smoke.Mock(m);
};
+Screw.Matchers.mock_function = function(func,name) {
+ return Smoke.MockFunction(func,name);
+};
+
Screw.Matchers.stub = function(obj, attr) {
return new Smoke.Stub(obj,attr);
};
View
45 lib/smoke.core.js
@@ -1,6 +1,49 @@
Smoke = {
print: function(v) {
// use the jquery print plugin if it is available or fall back to toString();
- return (jQuery && jQuery.print) ? $.print(v) : v.toString();
+ return (jQuery && jQuery.print) ? jQuery.print(v) : v.toString();
+ },
+
+ printArguments: function(args) {
+ var a = [];
+ if (args === undefined) args = '';
+ if ((args && args.callee) || (args instanceof Array)) {
+ for(var i = 0; i < args.length; i++) {
+ a.push(Smoke.print(args[i]));
+ }
+ } else {
+ // Workaround for jQuery.print returning "null" when called with an empty string.
+ if (!args && (typeof args == 'string')) {
+ a.push('');
+ } else {
+ a.push(Smoke.print(args));
+ }
+ }
+ return '(' + a.join(', ') + ')';
+ },
+
+ argumentsToArray: function(args) {
+ return Array.prototype.slice.call(args);
+ },
+
+ compare: function(a, b) {
+ if (a === b) return true;
+ if (a instanceof Array) {
+ if (b.length != a.length) return false;
+ for (var i = 0; i < b.length; i++)
+ if (!this.compare(a[i], b[i])) return false;
+ } else if (a instanceof Object) {
+ for (var key in a)
+ if (!this.compare(a[key], b[key])) return false;
+ for (var key in b)
+ if (!this.compare(b[key], a[key])) return false;
+ } else {
+ return false;
+ }
+ return true;
+ },
+
+ compareArguments: function(a, b) {
+ return this.compare(Smoke.argumentsToArray(a), Smoke.argumentsToArray(b));
}
};
View
119 lib/smoke.mock.js
@@ -10,6 +10,8 @@ Smoke.failed = function(mock, message){
// Some helpers
Smoke.reset = function(){
+ Smoke.mocks = Smoke.mocks || [];
+ for(var i=0; i<Smoke.mocks.length; i++) Smoke.mocks[i]._resetMocks();
Smoke.mocks = [];
Smoke.passCount = 0;
Smoke.failCount = 0;
@@ -17,47 +19,93 @@ Smoke.reset = function(){
Smoke.reset();
Smoke.checkExpectations = function(){
- for(var i=0; i<Smoke.mocks.length; i++) Smoke.mocks[i].checkExpectations();
+ for(var i=0; i<Smoke.mocks.length; i++) Smoke.mocks[i]._checkExpectations();
};
-// Don't play beyond here unless you know what you're doing
Smoke.Mock = function(originalObj) {
var obj = originalObj || {} ;
obj._expectations = {};
+ obj._valuesBeforeMocking = {};
+
obj.stub = function(attr){
return new Smoke.Stub(this, attr);
- },
+ };
+
obj.should_receive = function(attr){
var expectation = new Smoke.Mock.Expectation(this, attr);
- if(this._expectations[attr]==undefined) this._expectations[attr] = [];
- this._expectations[attr].push(expectation);
- var previousFunction = this[attr];
- var mock = this;
- this[attr] = function() {
- var result = expectation.run(arguments);
- if(result!=undefined) return result;
- return previousFunction!=undefined ? previousFunction.apply(mock,arguments) : undefined;
- };
+ this._expectations[attr] = (this._expectations[attr] || []).concat([expectation]);
+ this._valuesBeforeMocking[attr] = this[attr];
+ if(this._expectations[attr].length == 1) {
+ this[attr] = Smoke.Mock.Expectation.stub(this, attr);
+ }
return expectation;
- },
- obj.checkExpectations = function(){
+ };
+
+ obj._checkExpectations = function(){
for(var e in this._expectations) {
var expectations = this._expectations[e]
- for(var i in expectations) expectations[i].check();
+ for(var i=0; i < expectations.length; i++) expectations[i].check();
};
- },
+ };
+
+ obj._resetMocks = function(){
+ for(var attr in this._valuesBeforeMocking) {
+ this[attr] = this._valuesBeforeMocking[attr];
+ }
+
+ delete this._valuesBeforeMocking;
+ delete this._expectations;
+ delete this._resetMocks;
+ delete this._checkExpectations;
+ delete this.stub;
+ delete this.should_receive;
+ };
+
Smoke.mocks.push(obj);
return obj;
};
+Smoke.MockFunction = function(originalFunction, name) {
+ name = name || 'anonymous_function';
+ var mock = Smoke.Mock(function() {
+ var return_value = arguments.callee[name].apply(this, arguments);
+ if (return_value === undefined) {
+ return_value = (originalFunction || new Function()).apply(this, arguments)
+ }
+ return return_value;
+ });
+ mock[name] = (originalFunction || new Function());
+ mock.should_be_invoked = function() {
+ return this.should_receive(name);
+ }
+ return mock;
+};
+
Smoke.Mock.Expectation = function(mock, attr) {
this._mock = mock;
this._attr = attr;
this.callCount = 0;
this.returnValue = undefined;
- this.callerArgs = [];
+ this.callerArgs = undefined;
+ this.hasReturnValue = false;
};
+Smoke.Mock.Expectation.stub = function(mock, attr) {
+ return function() {
+ return function() {
+ var matched, return_value, args = arguments;
+ jQuery.each(this, function() {
+ this.run(args) && (matched = true) && (return_value = this.returnValue);
+ });
+ if (!matched) {
+ this[0].argumentMismatchError(args)
+ }
+ return return_value;
+ }.apply(mock._expectations[attr], arguments);
+ }
+}
+
+
Smoke.Mock.Expectation.prototype = {
exactly: function(count,type){
// type isn't used for now, it's just syntax ;)
@@ -78,13 +126,14 @@ Smoke.Mock.Expectation.prototype = {
return this
},
run: function(args){
- if(this.compareArrays(args, this.callerArgs)) {
- this.callCount+=1;
- return this.returnValue;
+ if((this.callerArgs === undefined) || Smoke.compareArguments(args, this.callerArgs)) {
+ return !!(this.callCount+=1);
};
+ return false
},
and_return: function(v){
- this.returnValue = v
+ this.hasReturnValue = true;
+ this.returnValue = v;
},
check: function(){
if(this.exactCount!=undefined) this.checkExactCount();
@@ -103,28 +152,20 @@ Smoke.Mock.Expectation.prototype = {
if(this.maxCount>=this.callCount) Smoke.passed(this);//console.log('Mock passed!')
else Smoke.failed(this, 'expected '+this.methodSignature()+' to be called at most '+this.maxCount+" times but it actually got called "+this.callCount+' times');
},
+ argumentMismatchError: function(args) {
+ Smoke.failed(this, 'expected ' + this._attr + ' with ' + Smoke.printArguments(this.callerArgs) + ' but received it with ' + Smoke.printArguments(args));
+ },
methodSignature: function(){
- var a = '';
- var args = this.callerArgs;
- for(var i=0; i<args.length; i++) a += Smoke.print(args[i])+', ';
- a =a.slice(0,-2);
- return this._attr+'('+a+')'
+ return this._attr + Smoke.printArguments(this.callerArgs);
},
parseCount: function(c){
switch(c){
- case 'once' : c=1; break;
- case 'twice' : c=2; break;
+ case 'once':
+ return 1;
+ case 'twice':
+ return 2;
+ default:
+ return c;
}
- return c;
- },
- compareArrays: function(a,b) {
- if (a.length != b.length) return false;
- for (var i = 0; i < b.length; i++) {
- if (a[i].compare) {
- if (!a[i].compare(b[i])) return false;
- }
- if (a[i] !== b[i]) return false;
- }
- return true;
}
};
View
14 spec/javascripts/fixtures/smoke.core.html
@@ -0,0 +1,14 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml">
+
+<head>
+ <title>Smoke Core | JavaScript Testing Results</title>
+ <link rel="stylesheet" href="../../../generators/blue_ridge/templates/screw.css" type="text/css" charset="utf-8" />
+ <script type="text/javascript" charset="utf-8">var BLUE_RIDGE_LIB_PREFIX = "../../lib/";</script>
+ <script type="text/javascript" src="../../../lib/blue-ridge.js"></script>
+</head>
+
+<body>
+ <!-- Put any HTML fixture elements here. -->
+</body>
+</html>
View
115 spec/javascripts/smoke.core_spec.js
@@ -0,0 +1,115 @@
+Screw.Unit(function() {
+ describe("core", function() {
+ var anonymous_function = function() { return arguments };
+ describe("printArguments", function() {
+ it("should return '()' is the arguments are empty", function() {
+ expect(Smoke.printArguments(arguments)).to(equal, '()');
+ });
+
+ it("should return '()' is the arguments are undefined", function() {
+ expect(Smoke.printArguments()).to(equal, '()');
+ });
+
+ it("should return the arguments comma seperated wrapped in parenthesis", function() {
+ var args = anonymous_function(1,2);
+ expect(Smoke.printArguments(args)).to(equal, '(1, 2)');
+ });
+
+ it("should handle being passed something other than an array or arguments object", function() {
+ expect(Smoke.printArguments(false)).to(equal, '(false)');
+ });
+ });
+
+ describe("argumentsToArray", function() {
+ it("should return an array", function() {
+ expect(Smoke.argumentsToArray(anonymous_function(1,2)) instanceof Array).to(equal, true);
+ });
+
+ it("should return the arguments in an array", function() {
+ expect(Smoke.argumentsToArray(anonymous_function(1,2))).to(equal, [1,2]);
+ });
+ });
+
+ describe("compare", function() {
+ describe("with arrays", function() {
+ var array = [1,2,3], nested_array = [[1,2], [3,4]];
+ it("should return true if the two arrays are equal", function() {
+ expect(Smoke.compare(array, [1,2,3])).to(equal, true);
+ });
+
+ it("should return true if the two nested arrays are equal", function() {
+ expect(Smoke.compare(nested_array, [[1,2], [3,4]])).to(equal, true);
+ });
+
+ it("should return false if the two arrays are not equal", function() {
+ expect(Smoke.compare(array, [1,2,3,4])).to(equal, false);
+ })
+
+ it("should return false if the two nested arrays are not equal", function() {
+ expect(Smoke.compare(nested_array, [[1,2],[3]])).to(equal, false);
+ })
+ });
+
+ describe("with objects", function() {
+ var object = { foo: 'bar' }, nested_object = { foo: { a: 'b' }, bar: { c: 'd'} };
+ it("should return true if the two objects are equal", function() {
+ expect(Smoke.compare(object, { foo: 'bar' })).to(equal, true);
+ });
+
+ it("should return true if the two nested objects are equal", function() {
+ expect(Smoke.compare(nested_object, { foo: { a: 'b' }, bar: { c: 'd'} })).to(equal, true);
+ });
+
+ it("should return false if the two objects are not equal", function() {
+ expect(Smoke.compare(object, {bar: 'foo'})).to(equal, false);
+ });
+
+ it("should return false if the two nested objects are not equal", function() {
+ expect(Smoke.compare(nested_object, { foo: { c: 'd' }, bar: { a: 'b' } })).to(equal, false);
+ });
+
+ it("should return false if an one of the objects has an additional property", function() {
+ expect(Smoke.compare(object, { foo: 'bar', bar: 'foo' })).to(equal, false);
+ })
+ });
+
+ describe('with value types', function() {
+ var string = 'foo', number = 1;
+ it("should return true if the two strings are equal", function() {
+ expect(Smoke.compare(string, 'foo')).to(equal, true);
+ });
+
+ it("should return true if the two numbers are equal", function() {
+ expect(Smoke.compare(number, 1)).to(equal, true);
+ });
+
+ it("should return false if the two strings are not equal", function() {
+ expect(Smoke.compare(string, 'bar')).to(equal, false);
+ });
+
+ it("should return false if the two number are not equal", function() {
+ expect(Smoke.compare(number, 2)).to(equal, false);
+ });
+ })
+
+ describe("with mixed types", function() {
+ var array = [1, { foo: 'bar'}, '2'], object = { foo: [1,2,3], bar: 'foo', one: 1 };
+ it("should return true if the two arrays with mixed types are equal", function() {
+ expect(Smoke.compare(array, [1, { foo: 'bar'}, '2'])).to(equal, true);
+ });
+
+ it("should return false if the two arrays with mixed types are not equal", function() {
+ expect(Smoke.compare(array, [1, { foo: 'bar'}, 3])).to(equal, false);
+ });
+
+ it("should return true if the two objects with mixed types are equal", function() {
+ expect(Smoke.compare(object, { foo: [1,2,3], bar: 'foo', one: 1 })).to(equal, true);
+ });
+
+ it("should return false if the two objects with mixed types are not equal", function() {
+ expect(Smoke.compare(object, { foo: [1,2,3], bar: 'foo', two: 3 })).to(equal, false);
+ });
+ })
+ });
+ });
+});
View
185 spec/javascripts/smoke.mock_spec.js
@@ -12,6 +12,38 @@ Screw.Unit(function() {
m.bar();
m.bar();
});
+
+ it("should fail when an expectation is called too many times", function() {
+ var m = mock();
+ m.should_receive('bar').exactly('once');
+ m.bar();
+ m.bar();
+ try {
+ Smoke.checkExpectations();
+ throw("exception");
+ } catch(e) {
+ Smoke.reset();
+ expect(e).to(equal, 'expected bar() to be called exactly 1 times but it got called 2 times');
+ }
+ });
+
+ it("should fail when an expectation is set and not called", function() {
+ var m = mock();
+ m.should_receive('bar').exactly('once');
+ try {
+ Smoke.checkExpectations();
+ throw("exception");
+ } catch(e) {
+ Smoke.reset();
+ expect(e).to(equal, 'expected bar() to be called exactly 1 times but it got called 0 times');
+ }
+ });
+
+ it("should not check arguments when with_arguments is not used", function() {
+ var m = mock()
+ m.should_receive('bar').exactly('once');
+ m.bar(1);
+ });
it("should check a minimum call count", function() {
var m = mock()
@@ -25,7 +57,7 @@ Screw.Unit(function() {
m.bar();
m.bar();
});
-
+
it("should allow return values directly from mocks",function() {
var m = mock()
m.should_receive('bar').exactly('once').and_return('hello');
@@ -40,10 +72,15 @@ Screw.Unit(function() {
mockObj.should_receive('foo').with_arguments('bar',baz).and_return('foobar');
expect(mockObj.foo('bar',baz)).to(equal, 'foobar');
});
- it("should return undefined if the arguments aren't matched", function() {
+
+ it("should throw an arguments mismatched error if the arguments aren't matched", function() {
mockObj = mock()
mockObj.should_receive('foo').with_arguments('bar').and_return('foobar');
- expect(mockObj.foo('chicken')).to(equal, undefined);
+ try {
+ mockObj.foo('chicken');
+ } catch(e) {
+ expect(e).to(equal, 'expected foo with ("bar") but received it with ("chicken")')
+ }
});
it("should allow mocking multiple method signatures with different returns", function() {
mockObj = mock()
@@ -59,7 +96,7 @@ Screw.Unit(function() {
});
});
- describe("added ontop of an existing object", function() {
+ describe("added on top of an existing object", function() {
before(function() {
obj = { say: "hello", shout: function() { return this.say.toUpperCase(); } }
mockObj = mock(obj);
@@ -84,13 +121,145 @@ Screw.Unit(function() {
expect(mockObj.length).to(equal,4);
});
- it("should place expectations on existing methods non-destructively", function() {
- myMock = mock({ say: "hello", shout: function() { return this.say.toUpperCase(); } });
+ it("should place expectations on existing methods destructively", function() {
+ myMock = mock({ say: "hello", shout: function() { throw "FAIL!" } });
myMock.should_receive('shout').exactly('once');
- expect(myMock.shout()).to(equal,'HELLO');
+ myMock.shout();
});
});
+ describe("proper teardown of mocks on global variables", function(){
+ var SomeGlobal = { say: "hello", shout: function() { return this.say.toUpperCase(); } };
+
+ it("when mocked in one test...", function(){
+ mock(SomeGlobal).should_receive("shout").and_return("some string");
+ expect(SomeGlobal.shout()).to(equal, "some string");
+ });
+
+ it("should not affect a later test", function(){
+ expect(SomeGlobal.shout()).to(equal, "HELLO");
+ });
+ });
+
+ describe("reseting mocks", function(){
+ it("should remove all mocking data from an object", function(){
+ var obj = { say: "hello", shout: function() { return this.say.toUpperCase(); } };
+ mock(obj).should_receive("shout").and_return("some string");
+
+ expect(obj._valuesBeforeMocking).to_not(equal, null);
+ expect(obj._expectations).to_not(equal, null);
+ expect(obj.stub).to_not(equal, null);
+ expect(obj.should_receive).to_not(equal, null);
+ expect(obj._checkExpectations).to_not(equal, null);
+ expect(obj._resetMocks).to_not(equal, null);
+
+ obj._resetMocks();
+ Smoke.mocks = [];
+
+ expect(obj._valuesBeforeMocking).to(equal, null);
+ expect(obj._expectations).to(equal, null);
+ expect(obj.stub).to(equal, null);
+ expect(obj.should_receive).to(equal, null);
+ expect(obj._checkExpectations).to(equal, null);
+ expect(obj._resetMocks).to(equal, null);
+ });
+
+ it("should replace the original functionality to the object", function(){
+ var obj = { say: "hello", shout: function() { return this.say.toUpperCase(); } };
+ mock(obj).should_receive("shout").and_return("some string");
+ expect(obj.shout()).to(equal, "some string");
+
+ obj._resetMocks();
+ Smoke.mocks = [];
+
+ expect(obj.shout()).to(equal, "HELLO");
+ });
+
+ });
+
+ describe("anonymous functions", function() {
+ before(function() {
+ foo = function() { return 'bar' };
+ mockObj = mock_function(foo);
+ });
+
+ it("should leave the original intact", function() {
+ expect(foo()).to(equal,'bar');
+ });
+
+ it("should still execute the mock like the original", function() {
+ expect(mockObj()).to(equal,'bar');
+ });
+
+ it("should still execute the mock like the original with arguments", function() {
+ var a = function(x,y,z) { return x+y+z };
+ aMock = mock_function(a)
+ expect(aMock('a','b','c')).to(equal,'abc');
+ });
+
+ it("should allow expectations to be set as usual", function() {
+ mockObj.should_receive('baz').exactly('once').and_return(1);
+ mockObj.baz()
+ });
+
+ it("should allow expectations to be set on invocations of itself", function() {
+ mockObj.should_be_invoked();
+ mockObj();
+ });
+
+ it("should allow expectation rules to be set", function() {
+ mockObj.should_be_invoked().exactly('twice').with_arguments('a');
+ mockObj('a');
+ mockObj('a');
+ });
+
+ it("should allow a return value to be set", function() {
+ mockObj.should_be_invoked().and_return('bar');
+ expect(mockObj('foo')).to(equal, 'bar');
+ });
+
+ it("should allow multiple return values to be set through the argument matchers", function() {
+ mockObj.should_be_invoked().with_arguments('foo').and_return('bar');
+ mockObj.should_be_invoked().with_arguments('bar').and_return('foo');
+ expect(mockObj('foo')).to(equal, 'bar');
+ expect(mockObj('bar')).to(equal, 'foo');
+ });
+
+ it("allows passing in a name for the function as a second argument to make error messages clearer", function() {
+ mock_function(foo, 'foo').should_be_invoked().exactly('once');
+ try {
+ Smoke.checkExpectations();
+ throw("exception");
+ } catch(e) {
+ Smoke.reset();
+ expect(e).to(equal, 'expected foo() to be called exactly 1 times but it got called 0 times');
+ }
+ });
+ });
+
+ describe("when array has been monkey-patched by js library not to be named here (grrr)", function() {
+ before(function() {
+ Array.prototype.remove = function() {
+ alert('I like monkeys!');
+ }
+ });
+ it("should not throw a type error when checking expectations", function() {
+ var m = mock()
+ m.should_receive('bar').at_least('once');
+ m.bar();
+ try {
+ Smoke.checkExpectations();
+ } catch(e) {
+ /* Make sure we clean up to not break the rest of the tests */
+ delete(Array.prototype.remove);
+ throw e;
+ }
+ });
+ after(function() {
+ delete(Array.prototype.remove);
+ });
+ });
+
describe("an objects prototype", function() {
it("should allow mocks to be carried through to individual objects", function() {
Aobj = function() {};
@@ -99,6 +268,6 @@ Screw.Unit(function() {
(new Aobj()).aFunction();
(new Aobj()).aFunction();
});
- });
+ });
});
});
Please sign in to comment.
Something went wrong with that request. Please try again.