Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Browse files

Merge branch 'andykent'

  • Loading branch information...
commit 9cee1c2c9257814beea66d0a3b64cb90931af39d 2 parents 1b7cdd5 + 80976a7
Larry Karnowski authored
View
13 README.markdown
@@ -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
2  lib/smoke.core.js
@@ -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
46 lib/smoke.mock.js
@@ -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
4 plugins/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
117 spec/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');
@@ -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() {};
View
144 spec/su/screw.css
@@ -1,90 +1,74 @@
-html {
- padding: 0.5em;
- font-family: Georgia, serif;
- background: #EDEBD5;
+* {
+ margin: 0;
+ padding: 0;
+ font-weight: normal;
+ list-style: none;
+ font-size: 1em;
}
- li {
- list-style-type: none;
- }
-
- .focused {
- background-color: #F4F2E4;
- }
-
- .focused * {
- opacity: 1.0;
- }
-
- h1, h2, p {
- opacity: 0.4;
- }
-
- .describes {
- padding-left: 0;
- }
-
- .describes h1 {
- font-size: 1.1em;
- color: #877C21;
- line-height: 1.8em;
- margin: 0pt 0pt 0.6em;
- border-bottom: 1px solid transparent;
- }
+body {
+ background: #fff;
+ color: #333;
+ font-family: "Helvetica Neue", Helvetica, Arial, "Sans-Serif";
+ font-size: 78.5%;
+ padding-top: 3em;
+}
- .describes h1:hover {
- cursor: pointer;
- color: #000;
- background-color: #F4F2E4;
- border-bottom: 1px solid #9A8E51;
- }
+h3.status {
+ position: fixed;
+ width: 100%;
+ top: 0;
+ left: 0;
+ margin: 0 0 1em 0;
+ padding: .75em;
+ font-size: 1.1em;
+ color: #fff;
+ background-color: #3875d7;
+ font-weight: normal;
+ -webkit-box-shadow: 0px 1px 5px #666;
+ -moz-box-shadow: 0px 2px 3px #999;
+}
- .describes .describe {
- margin-left: 0.6em;
- padding-left: 0.6em;
- border: 1px dashed grey;
- }
+ul {
+ margin: .5em 0;
+ padding: 0 1em;
+}
- .describes .describe .its {}
+h1 {
+ margin: 0 1em;
+ padding: .25em 0;
+ border-bottom: 1px dotted #ccc;
+ font-weight: bold;
+}
- .describes .describe .its .it {
- list-style-type: lower-roman;
- list-style-position: outside;
- }
+h2 {
+ padding: .2em .5em;
+ color: #ccc;
+}
- .describes .describe .its .it h2 {
- font-weight: normal;
- font-style: italic;
- padding-left: 0.5em;
- font-size: 1.0em;
- color: #877C21;
- line-height: 1.8em;
- margin: 0 0 0.5em;
- border-bottom: 1px solid transparent;
- }
-
- .describes .describe .its .it.enqueued h2 {
- background-color: #CC6600;
- color: white !important;
- }
+.enqueued h2 {
+ color: #999;
+}
- .describes .describe .its .it.passed h2 {
- background-color: #5A753D;
- color: white !important;
- }
+.passed h2 {
+ color: #333;
+ background: #abffa5;
+ border-top: 2px solid #ccffcc;
+ border-bottom: 2px solid #66ff66;
+}
- .describes .describe .its .it.failed h2 {
- background-color: #993300;
- color: white !important;
- }
+.failed h2 {
+ color: #333;
+ background: #ffaaaa;
+ border-top: 2px solid #ffcccc;
+ border-bottom: 2px solid #ff9977;
+}
- .describes .describe .its .it.failed p {
- margin-left: 1em;
- color: #993300;
- }
-
- .describes .describe .its .it h2:hover {
- cursor: pointer;
- color: #000 !important;
- border-bottom: 1px solid #9A8E51;
- }
+.error {
+ background-color: #f9f9f9;
+ margin-bottom: .5em;
+ padding: 1em;
+ border-left: 1px solid #ddd;
+ border-right: 1px solid #ddd;
+ border-bottom: 1px solid #ddd;
+}
Please sign in to comment.
Something went wrong with that request. Please try again.