-
-
Notifications
You must be signed in to change notification settings - Fork 771
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Fix #777: Support non-enumerable props when stubbing objects
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
1 parent
d9a42d3
commit 079b00f
Showing
5 changed files
with
239 additions
and
3 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
)); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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)); |