Skip to content

Commit 477064b

Browse files
authored
fix: make it possible to call through to underlying stub in stub instance (#2503)
* fix: make it possible to call through to underlying stub in stub instances refs #2477 refs #2501 * internal: Extract underlying createStubInstance * internal: extract tests into own module * internal: extract sinon type checking into own module closes #2501
1 parent 6e19746 commit 477064b

File tree

8 files changed

+246
-182
lines changed

8 files changed

+246
-182
lines changed

lib/sinon/create-stub-instance.js

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
"use strict";
2+
3+
const stub = require("./stub");
4+
const sinonType = require("./util/core/sinon-type");
5+
const forEach = require("@sinonjs/commons").prototypes.array.forEach;
6+
7+
function isStub(value) {
8+
return sinonType.get(value) === "stub";
9+
}
10+
11+
module.exports = function createStubInstance(constructor, overrides) {
12+
if (typeof constructor !== "function") {
13+
throw new TypeError("The constructor should be a function.");
14+
}
15+
16+
const stubInstance = Object.create(constructor.prototype);
17+
sinonType.set(stubInstance, "stub-instance");
18+
19+
const stubbedObject = stub(stubInstance);
20+
21+
forEach(Object.keys(overrides || {}), function (propertyName) {
22+
if (propertyName in stubbedObject) {
23+
var value = overrides[propertyName];
24+
if (isStub(value)) {
25+
stubbedObject[propertyName] = value;
26+
} else {
27+
stubbedObject[propertyName].returns(value);
28+
}
29+
} else {
30+
throw new Error(
31+
`Cannot stub ${propertyName}. Property does not exist!`
32+
);
33+
}
34+
});
35+
return stubbedObject;
36+
};

lib/sinon/sandbox.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ var sinonClock = require("./util/fake-timers");
1111
var sinonMock = require("./mock");
1212
var sinonSpy = require("./spy");
1313
var sinonStub = require("./stub");
14+
var sinonCreateStubInstance = require("./create-stub-instance");
1415
var sinonFake = require("./fake");
1516
var valueToString = require("@sinonjs/commons").valueToString;
1617
var fakeServer = require("nise").fakeServer;
@@ -71,7 +72,7 @@ function Sandbox() {
7172
};
7273

7374
sandbox.createStubInstance = function createStubInstance() {
74-
var stubbed = sinonStub.createStubInstance.apply(null, arguments);
75+
var stubbed = sinonCreateStubInstance.apply(null, arguments);
7576

7677
var ownMethods = collectOwnMethods(stubbed);
7778

lib/sinon/stub.js

Lines changed: 3 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ var spy = require("./spy");
1212
var extend = require("./util/core/extend");
1313
var getPropertyDescriptor = require("./util/core/get-property-descriptor");
1414
var isEsModule = require("./util/core/is-es-module");
15+
var sinonType = require("./util/core/sinon-type");
1516
var wrapMethod = require("./util/core/wrap-method");
1617
var throwOnFalsyObject = require("./throw-on-falsy-object");
1718
var valueToString = require("@sinonjs/commons").valueToString;
@@ -58,6 +59,8 @@ function createStub(originalFunc) {
5859
id: `stub#${uuid++}`,
5960
});
6061

62+
sinonType.set(proxy, "stub");
63+
6164
return proxy;
6265
}
6366

@@ -126,35 +129,6 @@ function stub(object, property) {
126129
return isStubbingNonFuncProperty ? s : wrapMethod(object, property, s);
127130
}
128131

129-
stub.createStubInstance = function (constructor, overrides) {
130-
if (typeof constructor !== "function") {
131-
throw new TypeError("The constructor should be a function.");
132-
}
133-
134-
// eslint-disable-next-line no-empty-function
135-
const noop = () => {};
136-
const defaultNoOpInstance = Object.create(constructor.prototype);
137-
walkObject((obj, prop) => (obj[prop] = noop), defaultNoOpInstance);
138-
139-
const stubbedObject = stub(defaultNoOpInstance);
140-
141-
forEach(Object.keys(overrides || {}), function (propertyName) {
142-
if (propertyName in stubbedObject) {
143-
var value = overrides[propertyName];
144-
if (value && value.createStubInstance) {
145-
stubbedObject[propertyName] = value;
146-
} else {
147-
stubbedObject[propertyName].returns(value);
148-
}
149-
} else {
150-
throw new Error(
151-
`Cannot stub ${propertyName}. Property does not exist!`
152-
);
153-
}
154-
});
155-
return stubbedObject;
156-
};
157-
158132
function assertValidPropertyDescriptor(descriptor, property) {
159133
if (!descriptor || !property) {
160134
return;

lib/sinon/util/core/sinon-type.js

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
"use strict";
2+
3+
const sinonTypeSymbolProperty = Symbol("SinonType");
4+
5+
module.exports = {
6+
/**
7+
* Set the type of a Sinon object to make it possible to identify it later at runtime
8+
*
9+
* @param {object|Function} object object/function to set the type on
10+
* @param {string} type the named type of the object/function
11+
*/
12+
set(object, type) {
13+
Object.defineProperty(object, sinonTypeSymbolProperty, {
14+
value: type,
15+
configurable: false,
16+
enumerable: false,
17+
});
18+
},
19+
get(object) {
20+
return object && object[sinonTypeSymbolProperty];
21+
},
22+
};

lib/sinon/util/core/wrap-method.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
"use strict";
22

3+
// eslint-disable-next-line no-empty-function
4+
const noop = () => {};
35
var getPropertyDescriptor = require("./get-property-descriptor");
46
var extend = require("./extend");
7+
const sinonType = require("./sinon-type");
58
var hasOwnProperty =
69
require("@sinonjs/commons").prototypes.object.hasOwnProperty;
710
var valueToString = require("@sinonjs/commons").valueToString;
@@ -230,6 +233,11 @@ module.exports = function wrapMethod(object, property, method) {
230233
}
231234
}
232235
}
236+
if (sinonType.get(object) === "stub-instance") {
237+
// this is simply to avoid errors after restoring if something should
238+
// traverse the object in a cleanup phase, ref #2477
239+
object[property] = noop;
240+
}
233241
}
234242

235243
return method;

test/create-stub-instance-test.js

Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
"use strict";
2+
3+
var referee = require("@sinonjs/referee");
4+
var createStub = require("../lib/sinon/stub");
5+
var createStubInstance = require("../lib/sinon/create-stub-instance");
6+
var assert = referee.assert;
7+
var refute = referee.refute;
8+
9+
describe("createStubInstance", function () {
10+
it("stubs existing methods", function () {
11+
var Class = function () {
12+
return;
13+
};
14+
Class.prototype.method = function () {
15+
return;
16+
};
17+
18+
var stub = createStubInstance(Class);
19+
stub.method.returns(3);
20+
assert.equals(3, stub.method());
21+
});
22+
23+
it("throws with no methods to stub", function () {
24+
var Class = function () {
25+
return;
26+
};
27+
28+
assert.exception(
29+
function () {
30+
createStubInstance(Class);
31+
},
32+
{
33+
message:
34+
"Found no methods on object to which we could apply mutations",
35+
}
36+
);
37+
});
38+
39+
it("doesn't call the constructor", function () {
40+
var Class = function (a, b) {
41+
var c = a + b;
42+
throw c;
43+
};
44+
Class.prototype.method = function () {
45+
return;
46+
};
47+
48+
var stub = createStubInstance(Class);
49+
refute.exception(function () {
50+
stub.method(3);
51+
});
52+
});
53+
54+
it("retains non function values", function () {
55+
var TYPE = "some-value";
56+
var Class = function () {
57+
return;
58+
};
59+
Class.prototype.method = function () {
60+
return;
61+
};
62+
Class.prototype.type = TYPE;
63+
64+
var stub = createStubInstance(Class);
65+
assert.equals(TYPE, stub.type);
66+
});
67+
68+
it("has no side effects on the prototype", function () {
69+
var proto = {
70+
method: function () {
71+
throw new Error("error");
72+
},
73+
};
74+
var Class = function () {
75+
return;
76+
};
77+
Class.prototype = proto;
78+
79+
var stub = createStubInstance(Class);
80+
refute.exception(stub.method);
81+
assert.exception(proto.method);
82+
});
83+
84+
it("throws exception for non function params", function () {
85+
var types = [{}, 3, "hi!"];
86+
87+
for (var i = 0; i < types.length; i++) {
88+
// yes, it's silly to create functions in a loop, it's also a test
89+
// eslint-disable-next-line no-loop-func
90+
assert.exception(function () {
91+
createStubInstance(types[i]);
92+
});
93+
}
94+
});
95+
96+
it("allows providing optional overrides", function () {
97+
var Class = function () {
98+
return;
99+
};
100+
Class.prototype.method = function () {
101+
return;
102+
};
103+
104+
var stub = createStubInstance(Class, {
105+
method: createStub().returns(3),
106+
});
107+
108+
assert.equals(3, stub.method());
109+
});
110+
111+
it("allows providing optional returned values", function () {
112+
var Class = function () {
113+
return;
114+
};
115+
Class.prototype.method = function () {
116+
return;
117+
};
118+
119+
var stub = createStubInstance(Class, {
120+
method: 3,
121+
});
122+
123+
assert.equals(3, stub.method());
124+
});
125+
126+
it("allows providing null as a return value", function () {
127+
var Class = function () {
128+
return;
129+
};
130+
Class.prototype.method = function () {
131+
return;
132+
};
133+
134+
var stub = createStubInstance(Class, {
135+
method: null,
136+
});
137+
138+
assert.equals(null, stub.method());
139+
});
140+
141+
it("throws an exception when trying to override non-existing property", function () {
142+
var Class = function () {
143+
return;
144+
};
145+
Class.prototype.method = function () {
146+
return;
147+
};
148+
149+
assert.exception(
150+
function () {
151+
createStubInstance(Class, {
152+
foo: createStub().returns(3),
153+
});
154+
},
155+
{ message: "Cannot stub foo. Property does not exist!" }
156+
);
157+
});
158+
});

test/issues/issues-test.js

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -805,4 +805,21 @@ describe("issues", function () {
805805
assert.isUndefined(restoredPropertyDescriptor);
806806
});
807807
});
808+
809+
describe("#2501 - createStubInstance stubs are not able to call through to the underlying function on the prototype", function () {
810+
it("should be able call through to the underlying function on the prototype", function () {
811+
class Foo {
812+
testMethod() {
813+
this.wasCalled = true;
814+
return 42;
815+
}
816+
}
817+
818+
const fooStubInstance = this.sandbox.createStubInstance(Foo);
819+
fooStubInstance.testMethod.callThrough();
820+
// const fooStubInstance = new Foo()
821+
fooStubInstance.testMethod();
822+
// assert.isTrue(fooStubInstance.wasCalled);
823+
});
824+
});
808825
});

0 commit comments

Comments
 (0)