Skip to content

Commit 721c850

Browse files
ErickWendelMoLow
authored andcommitted
test: fix mock.method to support class instances
It fixes a problem when trying to spy a method from a class instance or static functions on a class instance PR-URL: nodejs#45608 Reviewed-By: Colin Ihrig <[email protected]> Reviewed-By: Yagiz Nizipli <[email protected]> Reviewed-By: Benjamin Gruenbaum <[email protected]> Reviewed-By: Antoine du Hamel <[email protected]>
1 parent 30c5d6c commit 721c850

File tree

2 files changed

+176
-6
lines changed

2 files changed

+176
-6
lines changed

lib/internal/test_runner/mock.js

+25-5
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ const {
66
FunctionPrototypeCall,
77
ObjectDefineProperty,
88
ObjectGetOwnPropertyDescriptor,
9+
ObjectGetPrototypeOf,
910
Proxy,
1011
ReflectApply,
1112
ReflectConstruct,
@@ -130,13 +131,15 @@ class MockTracker {
130131
}
131132

132133
method(
133-
object,
134+
objectOrFunction,
134135
methodName,
135136
implementation = kDefaultFunction,
136137
options = kEmptyObject,
137138
) {
138-
validateObject(object, 'object');
139139
validateStringOrSymbol(methodName, 'methodName');
140+
if (typeof objectOrFunction !== 'function') {
141+
validateObject(objectOrFunction, 'object');
142+
}
140143

141144
if (implementation !== null && typeof implementation === 'object') {
142145
options = implementation;
@@ -161,8 +164,8 @@ class MockTracker {
161164
'options.setter', setter, "cannot be used with 'options.getter'"
162165
);
163166
}
167+
const descriptor = findMethodOnPrototypeChain(objectOrFunction, methodName);
164168

165-
const descriptor = ObjectGetOwnPropertyDescriptor(object, methodName);
166169
let original;
167170

168171
if (getter) {
@@ -179,7 +182,7 @@ class MockTracker {
179182
);
180183
}
181184

182-
const restore = { descriptor, object, methodName };
185+
const restore = { descriptor, object: objectOrFunction, methodName };
183186
const impl = implementation === kDefaultFunction ?
184187
original : implementation;
185188
const ctx = new MockFunctionContext(impl, restore, times);
@@ -201,7 +204,7 @@ class MockTracker {
201204
mockDescriptor.value = mock;
202205
}
203206

204-
ObjectDefineProperty(object, methodName, mockDescriptor);
207+
ObjectDefineProperty(objectOrFunction, methodName, mockDescriptor);
205208

206209
return mock;
207210
}
@@ -350,4 +353,21 @@ function validateTimes(value, name) {
350353
validateInteger(value, name, 1);
351354
}
352355

356+
function findMethodOnPrototypeChain(instance, methodName) {
357+
let host = instance;
358+
let descriptor;
359+
360+
while (host !== null) {
361+
descriptor = ObjectGetOwnPropertyDescriptor(host, methodName);
362+
363+
if (descriptor) {
364+
break;
365+
}
366+
367+
host = ObjectGetPrototypeOf(host);
368+
}
369+
370+
return descriptor;
371+
}
372+
353373
module.exports = { MockTracker };

test/parallel/test-runner-mocking.js

+151-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22
const common = require('../common');
33
const assert = require('node:assert');
44
const { mock, test } = require('node:test');
5-
65
test('spies on a function', (t) => {
76
const sum = t.mock.fn((arg1, arg2) => {
87
return arg1 + arg2;
@@ -319,6 +318,157 @@ test('spy functions can be bound', (t) => {
319318
assert.strictEqual(sum.bind(0)(2, 11), 13);
320319
});
321320

321+
test('mocks prototype methods on an instance', async (t) => {
322+
class Runner {
323+
async someTask(msg) {
324+
return Promise.resolve(msg);
325+
}
326+
327+
async method(msg) {
328+
await this.someTask(msg);
329+
return msg;
330+
}
331+
}
332+
const msg = 'ok';
333+
const obj = new Runner();
334+
assert.strictEqual(await obj.method(msg), msg);
335+
336+
t.mock.method(obj, obj.someTask.name);
337+
assert.strictEqual(obj.someTask.mock.calls.length, 0);
338+
339+
assert.strictEqual(await obj.method(msg), msg);
340+
341+
const call = obj.someTask.mock.calls[0];
342+
343+
assert.deepStrictEqual(call.arguments, [msg]);
344+
assert.strictEqual(await call.result, msg);
345+
assert.strictEqual(call.target, undefined);
346+
assert.strictEqual(call.this, obj);
347+
348+
const obj2 = new Runner();
349+
// Ensure that a brand new instance is not mocked
350+
assert.strictEqual(
351+
obj2.someTask.mock,
352+
undefined
353+
);
354+
355+
assert.strictEqual(obj.someTask.mock.restore(), undefined);
356+
assert.strictEqual(await obj.method(msg), msg);
357+
assert.strictEqual(obj.someTask.mock, undefined);
358+
assert.strictEqual(Runner.prototype.someTask.mock, undefined);
359+
});
360+
361+
test('spies on async static class methods', async (t) => {
362+
class Runner {
363+
static async someTask(msg) {
364+
return Promise.resolve(msg);
365+
}
366+
367+
static async method(msg) {
368+
await this.someTask(msg);
369+
return msg;
370+
}
371+
}
372+
const msg = 'ok';
373+
assert.strictEqual(await Runner.method(msg), msg);
374+
375+
t.mock.method(Runner, Runner.someTask.name);
376+
assert.strictEqual(Runner.someTask.mock.calls.length, 0);
377+
378+
assert.strictEqual(await Runner.method(msg), msg);
379+
380+
const call = Runner.someTask.mock.calls[0];
381+
382+
assert.deepStrictEqual(call.arguments, [msg]);
383+
assert.strictEqual(await call.result, msg);
384+
assert.strictEqual(call.target, undefined);
385+
assert.strictEqual(call.this, Runner);
386+
387+
assert.strictEqual(Runner.someTask.mock.restore(), undefined);
388+
assert.strictEqual(await Runner.method(msg), msg);
389+
assert.strictEqual(Runner.someTask.mock, undefined);
390+
assert.strictEqual(Runner.prototype.someTask, undefined);
391+
392+
});
393+
394+
test('given null to a mock.method it throws a invalid argument error', (t) => {
395+
assert.throws(() => t.mock.method(null, {}), { code: 'ERR_INVALID_ARG_TYPE' });
396+
});
397+
398+
test('it should throw given an inexistent property on a object instance', (t) => {
399+
assert.throws(() => t.mock.method({ abc: 0 }, 'non-existent'), {
400+
code: 'ERR_INVALID_ARG_VALUE'
401+
});
402+
});
403+
404+
test('spy functions can be used on classes inheritance', (t) => {
405+
// Makes sure that having a null-prototype doesn't throw our system off
406+
class A extends null {
407+
static someTask(msg) {
408+
return msg;
409+
}
410+
static method(msg) {
411+
return this.someTask(msg);
412+
}
413+
}
414+
class B extends A {}
415+
class C extends B {}
416+
417+
const msg = 'ok';
418+
assert.strictEqual(C.method(msg), msg);
419+
420+
t.mock.method(C, C.someTask.name);
421+
assert.strictEqual(C.someTask.mock.calls.length, 0);
422+
423+
assert.strictEqual(C.method(msg), msg);
424+
425+
const call = C.someTask.mock.calls[0];
426+
427+
assert.deepStrictEqual(call.arguments, [msg]);
428+
assert.strictEqual(call.result, msg);
429+
assert.strictEqual(call.target, undefined);
430+
assert.strictEqual(call.this, C);
431+
432+
assert.strictEqual(C.someTask.mock.restore(), undefined);
433+
assert.strictEqual(C.method(msg), msg);
434+
assert.strictEqual(C.someTask.mock, undefined);
435+
});
436+
437+
test('spy functions don\'t affect the prototype chain', (t) => {
438+
439+
class A {
440+
static someTask(msg) {
441+
return msg;
442+
}
443+
}
444+
class B extends A {}
445+
class C extends B {}
446+
447+
const msg = 'ok';
448+
449+
const ABeforeMockIsUnchanged = Object.getOwnPropertyDescriptor(A, A.someTask.name);
450+
const BBeforeMockIsUnchanged = Object.getOwnPropertyDescriptor(B, B.someTask.name);
451+
const CBeforeMockShouldNotHaveDesc = Object.getOwnPropertyDescriptor(C, C.someTask.name);
452+
453+
t.mock.method(C, C.someTask.name);
454+
C.someTask(msg);
455+
const BAfterMockIsUnchanged = Object.getOwnPropertyDescriptor(B, B.someTask.name);
456+
457+
const AAfterMockIsUnchanged = Object.getOwnPropertyDescriptor(A, A.someTask.name);
458+
const CAfterMockHasDescriptor = Object.getOwnPropertyDescriptor(C, C.someTask.name);
459+
460+
assert.strictEqual(CBeforeMockShouldNotHaveDesc, undefined);
461+
assert.ok(CAfterMockHasDescriptor);
462+
463+
assert.deepStrictEqual(ABeforeMockIsUnchanged, AAfterMockIsUnchanged);
464+
assert.strictEqual(BBeforeMockIsUnchanged, BAfterMockIsUnchanged);
465+
assert.strictEqual(BBeforeMockIsUnchanged, undefined);
466+
467+
assert.strictEqual(C.someTask.mock.restore(), undefined);
468+
const CAfterRestoreKeepsDescriptor = Object.getOwnPropertyDescriptor(C, C.someTask.name);
469+
assert.ok(CAfterRestoreKeepsDescriptor);
470+
});
471+
322472
test('mocked functions report thrown errors', (t) => {
323473
const testError = new Error('test error');
324474
const fn = t.mock.fn(() => {

0 commit comments

Comments
 (0)