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)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy