Skip to content

Commit 477064b

Browse files
authoredMar 26, 2023
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

8 files changed

+246
-182
lines changed
 

‎lib/sinon/create-stub-instance.js

+36
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

+2-1
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

+3-29
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

+22
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

+8
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

+158
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

+17
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
});

‎test/stub-test.js

-152
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22

33
var referee = require("@sinonjs/referee");
44
var createStub = require("../lib/sinon/stub");
5-
var createStubInstance = require("../lib/sinon/stub").createStubInstance;
65
var createSpy = require("../lib/sinon/spy");
76
var createProxy = require("../lib/sinon/proxy");
87
var match = require("@sinonjs/samsam").createMatcher;
@@ -3139,157 +3138,6 @@ describe("stub", function () {
31393138
});
31403139
});
31413140

3142-
describe(".createStubInstance", function () {
3143-
it("stubs existing methods", function () {
3144-
var Class = function () {
3145-
return;
3146-
};
3147-
Class.prototype.method = function () {
3148-
return;
3149-
};
3150-
3151-
var stub = createStubInstance(Class);
3152-
stub.method.returns(3);
3153-
assert.equals(3, stub.method());
3154-
});
3155-
3156-
it("throws with no methods to stub", function () {
3157-
var Class = function () {
3158-
return;
3159-
};
3160-
3161-
assert.exception(
3162-
function () {
3163-
createStubInstance(Class);
3164-
},
3165-
{
3166-
message:
3167-
"Found no methods on object to which we could apply mutations",
3168-
}
3169-
);
3170-
});
3171-
3172-
it("doesn't call the constructor", function () {
3173-
var Class = function (a, b) {
3174-
var c = a + b;
3175-
throw c;
3176-
};
3177-
Class.prototype.method = function () {
3178-
return;
3179-
};
3180-
3181-
var stub = createStubInstance(Class);
3182-
refute.exception(function () {
3183-
stub.method(3);
3184-
});
3185-
});
3186-
3187-
it("retains non function values", function () {
3188-
var TYPE = "some-value";
3189-
var Class = function () {
3190-
return;
3191-
};
3192-
Class.prototype.method = function () {
3193-
return;
3194-
};
3195-
Class.prototype.type = TYPE;
3196-
3197-
var stub = createStubInstance(Class);
3198-
assert.equals(TYPE, stub.type);
3199-
});
3200-
3201-
it("has no side effects on the prototype", function () {
3202-
var proto = {
3203-
method: function () {
3204-
throw new Error("error");
3205-
},
3206-
};
3207-
var Class = function () {
3208-
return;
3209-
};
3210-
Class.prototype = proto;
3211-
3212-
var stub = createStubInstance(Class);
3213-
refute.exception(stub.method);
3214-
assert.exception(proto.method);
3215-
});
3216-
3217-
it("throws exception for non function params", function () {
3218-
var types = [{}, 3, "hi!"];
3219-
3220-
for (var i = 0; i < types.length; i++) {
3221-
// yes, it's silly to create functions in a loop, it's also a test
3222-
// eslint-disable-next-line no-loop-func
3223-
assert.exception(function () {
3224-
createStubInstance(types[i]);
3225-
});
3226-
}
3227-
});
3228-
3229-
it("allows providing optional overrides", function () {
3230-
var Class = function () {
3231-
return;
3232-
};
3233-
Class.prototype.method = function () {
3234-
return;
3235-
};
3236-
3237-
var stub = createStubInstance(Class, {
3238-
method: createStub().returns(3),
3239-
});
3240-
3241-
assert.equals(3, stub.method());
3242-
});
3243-
3244-
it("allows providing optional returned values", function () {
3245-
var Class = function () {
3246-
return;
3247-
};
3248-
Class.prototype.method = function () {
3249-
return;
3250-
};
3251-
3252-
var stub = createStubInstance(Class, {
3253-
method: 3,
3254-
});
3255-
3256-
assert.equals(3, stub.method());
3257-
});
3258-
3259-
it("allows providing null as a return value", function () {
3260-
var Class = function () {
3261-
return;
3262-
};
3263-
Class.prototype.method = function () {
3264-
return;
3265-
};
3266-
3267-
var stub = createStubInstance(Class, {
3268-
method: null,
3269-
});
3270-
3271-
assert.equals(null, stub.method());
3272-
});
3273-
3274-
it("throws an exception when trying to override non-existing property", function () {
3275-
var Class = function () {
3276-
return;
3277-
};
3278-
Class.prototype.method = function () {
3279-
return;
3280-
};
3281-
3282-
assert.exception(
3283-
function () {
3284-
createStubInstance(Class, {
3285-
foo: createStub().returns(3),
3286-
});
3287-
},
3288-
{ message: "Cannot stub foo. Property does not exist!" }
3289-
);
3290-
});
3291-
});
3292-
32933141
describe(".callThrough", function () {
32943142
it("does not call original function when arguments match conditional stub", function () {
32953143
// We need a function here because we can't wrap properties that are already stubs

0 commit comments

Comments
 (0)
Please sign in to comment.