Permalink
Browse files

Merge branch 'andykent'

  • Loading branch information...
2 parents 1b7cdd5 + 80976a7 commit 9cee1c2c9257814beea66d0a3b64cb90931af39d Larry Karnowski committed Jul 11, 2009
Showing with 227 additions and 99 deletions.
  1. +11 −2 README.markdown
  2. +1 −1 lib/smoke.core.js
  3. +34 −12 lib/smoke.mock.js
  4. +4 −0 plugins/screw.mocking.js
  5. +113 −4 spec/mock_spec.js
  6. +64 −80 spec/su/screw.css
View
@@ -1,6 +1,7 @@
Smoke
=====
-Smoke is a JavaScript mocking and stubbing framework. It has a familiar RSpec style interface and whilst it is perfectly capable of being used as a free-standing tool it is currently most useful when used in conjunction with the screw-unit testing framework.
+Smoke is a JavaScript mocking and stubbing framework. It has a familiar RSpec style interface and whilst it is perfectly capable of being used as a free-standing tool it comes complete with a plugin for the screw-unit testing framework.
+Below are some getting started guides to get you on your way. See the included specs for further help.
Getting Started (With Screw.Unit)
---------------------------------
@@ -15,6 +16,13 @@ but you will need to include the Smoke files and you can get straight to work us
myMock.should_receive('foo').at_least('once').and_return('hello');
expect(foo.baz).to(equal, 'hi!');
+Additionally Smoke now has a macro for mocking anonymous functions this is great for mocking and stubbing closures...
+
+ callback = mock_function();
+ callback.should_be_invoked().exactly('once');
+ $.bind('click', callback);
+ $.trigger('click');
+
Getting Started (Free Standing)
-------------------------------
Include the library files in your document...
@@ -32,6 +40,7 @@ Create your stubs...
Create your mocks...
var myMock = Smoke.Mock();
+ var myMockFunction = Smoke.MockFunction();
Create your expectations...
@@ -48,7 +57,7 @@ Mocks are the main part of the Smoke framework. But Smoke.Mock() has a bit of a
1. *without any arguments* it will return a fresh Mock with no more than default Object methods. You will need to mock all your interactions and check all your expectations.
2. *with an argument* it will return a 'Mocked' version of the object that was passed in. This is very useful if you just want to mock a single method on your object whilst leaving the rest intact. It's especially helpful for just carrying out expectations on existing objects without any mocking at all.
-Smoke expectations are non-destructive meaning that if you add an expectation but don't specify a return value then the previous method will still be invoked (if one exists) and it's result will be returned.
+Smoke expectations are now destructive meaning that if you add an expectation then the previous method will not be invoked (if one exists) and undefined will be returned (unless you specify otherwise using and_return()).
Known Issues
------------
View
@@ -1,6 +1,6 @@
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();
}
};
View
@@ -26,36 +26,56 @@ Smoke.Mock = function(originalObj) {
obj._expectations = {};
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;
+ if(this._expectations[attr].length>1) return expectation;
this[attr] = function() {
- var result = expectation.run(arguments);
- if(result!=undefined) return result;
- return previousFunction!=undefined ? previousFunction.apply(mock,arguments) : undefined;
+ var ret = undefined;
+ for(var i=0; i<mock._expectations[attr].length; i++) {
+ var expectation = mock._expectations[attr][i];
+ var ran = expectation.run(arguments);
+ if(ran && expectation.hasReturnValue) ret = expectation.returnValue;
+ };
+ return ret;
};
return expectation;
- },
+ };
+
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();
};
- },
+ };
+
Smoke.mocks.push(obj);
return obj;
};
+Smoke.MockFunction = function(originalFunction, name) {
+ var name = name || 'anonymous_function';
+ var originalFunction = originalFunction || function() {};
+ var scope = function() { return scope.mockFunction.apply(this,arguments) };
+ scope[name] = originalFunction;
+ scope.mockFunction = function() { return scope[name].apply(this,arguments); };
+ var mock = Smoke.Mock(scope);
+ mock.should_be_invoked = function() { return mock.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.prototype = {
@@ -78,13 +98,15 @@ Smoke.Mock.Expectation.prototype = {
return this
},
run: function(args){
- if(this.compareArrays(args, this.callerArgs)) {
+ if(typeof(this.callerArgs) == 'undefined' || this.compareArrays(args, this.callerArgs)) {
this.callCount+=1;
- return this.returnValue;
+ return true;
};
+ return false
},
and_return: function(v){
- this.returnValue = v
+ this.hasReturnValue = true;
+ this.returnValue = v;
},
check: function(){
if(this.exactCount!=undefined) this.checkExactCount();
@@ -105,7 +127,7 @@ Smoke.Mock.Expectation.prototype = {
},
methodSignature: function(){
var a = '';
- var args = this.callerArgs;
+ 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+')'
View
@@ -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
@@ -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');
@@ -57,6 +89,12 @@ Screw.Unit(function() {
mockObj.should_receive('foo').with_arguments('bar').exactly('once');
mockObj.foo('bar')
});
+ it("should only mock the exact method signature when with_arguments is used with no arguments", function() {
+ mockObj = mock();
+ mockObj.should_receive('foo').with_arguments().exactly('once');
+ mockObj.foo('should ignore this call');
+ mockObj.foo();
+ });
});
describe("added ontop of an existing object", function() {
@@ -84,13 +122,84 @@ 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("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("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() {};
Oops, something went wrong.

0 comments on commit 9cee1c2

Please sign in to comment.