Skip to content

Commit

Permalink
Fix #777: Support non-enumerable props when stubbing objects
Browse files Browse the repository at this point in the history
This commit modifies sinon.stub() such that when only an object is
given, all non-enumerable properties as well as enumerable properties
that are functions will be stubbed.

The iteration over the object is accomplished by using sinon.walk, a new
utility function that supports iterating over all properties of an
object up the prototype chain, falling back to for..in when
getOwnPropertyNames is not available.

This functionality allows sinon.stub() - and especially methods like
sinon.createStubInstance() - to be future-forward by facilitating the
fact that methods defined in class declarations are non-enumerable by
default. It also allows stubbing of native objects such as Arrays and
Dates without having to explictly pass property names.
  • Loading branch information
Travis Kaufman authored and traviskaufman committed Aug 29, 2015
1 parent d9a42d3 commit 079b00f
Show file tree
Hide file tree
Showing 5 changed files with 239 additions and 3 deletions.
1 change: 1 addition & 0 deletions lib/sinon.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ var sinon = (function () { // eslint-disable-line no-unused-vars
function loadDependencies(require, exports, module) {
sinonModule = module.exports = require("./sinon/util/core");
require("./sinon/extend");
require("./sinon/walk");
require("./sinon/typeOf");
require("./sinon/times_in_words");
require("./sinon/spy");
Expand Down
12 changes: 9 additions & 3 deletions lib/sinon/stub.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
* @depend extend.js
* @depend spy.js
* @depend behavior.js
* @depend walk.js
*/
/**
* Stub functions
Expand Down Expand Up @@ -49,11 +50,16 @@
}

if (typeof property === "undefined" && typeof object === "object") {
for (prop in object) {
if (typeof sinon.getPropertyDescriptor(object, prop).value === "function") {
sinon.walk(object || {}, function (value, prop, propOwner) {
// we don't want to stub things like toString(), valueOf(), etc. so we only stub if the object
// is not Object.prototype
if (
propOwner !== Object.prototype &&
typeof sinon.getPropertyDescriptor(propOwner, prop).value === "function"
) {
stub(object, prop);
}
}
});

return object;
}
Expand Down
70 changes: 70 additions & 0 deletions lib/sinon/walk.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
/**
* @depend util/core.js
*/
(function (sinonGlobal) {
"use strict";

function makeApi(sinon) {
/* Public: walks the prototype chain of an object and iterates over every own property
* name encountered. The iterator is called in the same fashion that Array.prototype.forEach
* works, where it is passed the value, key, and own object as the 1st, 2nd, and 3rd positional
* argument, respectively. In cases where Object.getOwnPropertyNames is not available, walk will
* default to using a simple for..in loop.
*
* obj - The object to walk the prototype chain for.
* iterator - The function to be called on each pass of the walk.
* context - (Optional) When given, the iterator will be called with this object as the receiver.
*/
function walk(obj, iterator, context) {
var proto, prop;

if (typeof Object.getOwnPropertyNames !== "function") {
// We explicitly want to enumerate through all of the prototype's properties
// in this case, therefore we deliberately leave out an own property check.
/* eslint-disable guard-for-in */
for (prop in obj) {
iterator.call(context, obj[prop], prop, obj);
}
/* eslint-enable guard-for-in */

return;
}

Object.getOwnPropertyNames(obj).forEach(function (k) {
iterator.call(context, obj[k], k, obj);
});

proto = Object.getPrototypeOf(obj);
if (proto) {
walk(proto, iterator, context);
}
}

sinon.walk = walk;
return sinon.walk;
}

function loadDependencies(require, exports, module) {
var sinon = require("./util/core");
module.exports = makeApi(sinon);
}

var isNode = typeof module !== "undefined" && module.exports && typeof require === "function";
var isAMD = typeof define === "function" && typeof define.amd === "object" && define.amd;

if (isAMD) {
define(loadDependencies);
return;
}

if (isNode) {
loadDependencies(require, module.exports, module);
return;
}

if (sinonGlobal) {
makeApi(sinonGlobal);
}
}(
typeof sinon === "object" && sinon // eslint-disable-line no-undef
));
44 changes: 44 additions & 0 deletions test/stub-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -646,6 +646,50 @@
sinon.stub(object);

assert.equals(object.foo, "bar");
},

"handles non-enumerable properties": function () {
var obj = {
func1: function () {},
func2: function () {}
};

Object.defineProperty(obj, "func3", {
value: function () {},
writable: true,
configurable: true
});

sinon.stub(obj);

assert.isFunction(obj.func1.restore);
assert.isFunction(obj.func2.restore);
assert.isFunction(obj.func3.restore);
},

"handles non-enumerable properties on prototypes": function () {
function Obj() {}
Object.defineProperty(Obj.prototype, "func1", {
value: function () {},
writable: true,
configurable: true
});

var obj = new Obj();

sinon.stub(obj);

assert.isFunction(obj.func1.restore);
},

"does not stub non-enumerable properties from Object.prototype": function () {
var obj = {};

sinon.stub(obj);

refute.isFunction(obj.toString.restore);
refute.isFunction(obj.toLocaleString.restore);
refute.isFunction(obj.propertyIsEnumerable.restore);
}
},

Expand Down
115 changes: 115 additions & 0 deletions test/walk-test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
(function (root) {
"use strict";

var buster = root.buster || require("buster"),
sinon = root.sinon || require("../lib/sinon"),
assert = buster.assert;

buster.testCase("sinon.walk", {
"should call iterator with value, key, and obj, with context as the receiver": function () {
var target = Object.create(null),
rcvr = {},
iterator = sinon.spy();

target.hello = "world";
target.foo = 15;

sinon.walk(target, iterator, rcvr);

assert(iterator.calledTwice);
assert(iterator.alwaysCalledOn(rcvr));
assert(iterator.calledWithExactly("world", "hello", target));
assert(iterator.calledWithExactly(15, "foo", target));
},

"should work with non-enumerable properties": function () {
var target = Object.create(null),
iterator = sinon.spy();

target.hello = "world";
Object.defineProperty(target, "foo", {
value: 15
});

sinon.walk(target, iterator);

assert(iterator.calledTwice);
assert(iterator.calledWith("world", "hello"));
assert(iterator.calledWith(15, "foo"));
},

"should walk the prototype chain of an object": function () {
var parentProto, proto, target, iterator;

parentProto = Object.create(null, {
nonEnumerableParentProp: {
value: "non-enumerable parent prop"
},
enumerableParentProp: {
value: "enumerable parent prop",
enumerable: true
}
});

proto = Object.create(parentProto, {
nonEnumerableProp: {
value: "non-enumerable prop"
},
enumerableProp: {
value: "enumerable prop",
enumerable: true
}
});

target = Object.create(proto, {
nonEnumerableOwnProp: {
value: "non-enumerable own prop"
},
enumerableOwnProp: {
value: "enumerable own prop",
enumerable: true
}
});

iterator = sinon.spy();

sinon.walk(target, iterator);

assert.equals(iterator.callCount, 6);
assert(iterator.calledWith("non-enumerable own prop", "nonEnumerableOwnProp", target));
assert(iterator.calledWith("enumerable own prop", "enumerableOwnProp", target));
assert(iterator.calledWith("non-enumerable prop", "nonEnumerableProp", proto));
assert(iterator.calledWith("enumerable prop", "enumerableProp", proto));
assert(iterator.calledWith("non-enumerable parent prop", "nonEnumerableParentProp", parentProto));
assert(iterator.calledWith("enumerable parent prop", "enumerableParentProp", parentProto));
},

"should fall back to for..in if getOwnPropertyNames is not available": function () {
var getOwnPropertyNames = Object.getOwnPropertyNames,
Target = function Target() {
this.hello = "world";
},
target = new Target(),
rcvr = {},
iterator = sinon.spy(),
err = null;

Target.prototype.foo = 15;
delete Object.getOwnPropertyNames;

try {
sinon.walk(target, iterator, rcvr);
assert(iterator.calledTwice);
assert(iterator.alwaysCalledOn(rcvr));
assert(iterator.calledWith("world", "hello"));
assert(iterator.calledWith(15, "foo"));
} catch(e) {
err = e;
} finally {
Object.getOwnPropertyNames = getOwnPropertyNames;
}

assert.isNull(err, "sinon.walk tests failed with message '" + (err && err.message) + "'");
}
});
}(this));

0 comments on commit 079b00f

Please sign in to comment.