mirror of
https://github.com/zebrajr/node.git
synced 2026-01-15 12:15:26 +00:00
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: https://github.com/nodejs/node/pull/45608 Reviewed-By: Colin Ihrig <cjihrig@gmail.com> Reviewed-By: Yagiz Nizipli <yagiz@nizipli.com> Reviewed-By: Benjamin Gruenbaum <benjamingr@gmail.com> Reviewed-By: Antoine du Hamel <duhamelantoine1995@gmail.com>
This commit is contained in:
@@ -6,6 +6,7 @@ const {
|
||||
FunctionPrototypeCall,
|
||||
ObjectDefineProperty,
|
||||
ObjectGetOwnPropertyDescriptor,
|
||||
ObjectGetPrototypeOf,
|
||||
Proxy,
|
||||
ReflectApply,
|
||||
ReflectConstruct,
|
||||
@@ -130,13 +131,15 @@ class MockTracker {
|
||||
}
|
||||
|
||||
method(
|
||||
object,
|
||||
objectOrFunction,
|
||||
methodName,
|
||||
implementation = kDefaultFunction,
|
||||
options = kEmptyObject,
|
||||
) {
|
||||
validateObject(object, 'object');
|
||||
validateStringOrSymbol(methodName, 'methodName');
|
||||
if (typeof objectOrFunction !== 'function') {
|
||||
validateObject(objectOrFunction, 'object');
|
||||
}
|
||||
|
||||
if (implementation !== null && typeof implementation === 'object') {
|
||||
options = implementation;
|
||||
@@ -161,8 +164,8 @@ class MockTracker {
|
||||
'options.setter', setter, "cannot be used with 'options.getter'"
|
||||
);
|
||||
}
|
||||
const descriptor = findMethodOnPrototypeChain(objectOrFunction, methodName);
|
||||
|
||||
const descriptor = ObjectGetOwnPropertyDescriptor(object, methodName);
|
||||
let original;
|
||||
|
||||
if (getter) {
|
||||
@@ -179,7 +182,7 @@ class MockTracker {
|
||||
);
|
||||
}
|
||||
|
||||
const restore = { descriptor, object, methodName };
|
||||
const restore = { descriptor, object: objectOrFunction, methodName };
|
||||
const impl = implementation === kDefaultFunction ?
|
||||
original : implementation;
|
||||
const ctx = new MockFunctionContext(impl, restore, times);
|
||||
@@ -201,7 +204,7 @@ class MockTracker {
|
||||
mockDescriptor.value = mock;
|
||||
}
|
||||
|
||||
ObjectDefineProperty(object, methodName, mockDescriptor);
|
||||
ObjectDefineProperty(objectOrFunction, methodName, mockDescriptor);
|
||||
|
||||
return mock;
|
||||
}
|
||||
@@ -350,4 +353,21 @@ function validateTimes(value, name) {
|
||||
validateInteger(value, name, 1);
|
||||
}
|
||||
|
||||
function findMethodOnPrototypeChain(instance, methodName) {
|
||||
let host = instance;
|
||||
let descriptor;
|
||||
|
||||
while (host !== null) {
|
||||
descriptor = ObjectGetOwnPropertyDescriptor(host, methodName);
|
||||
|
||||
if (descriptor) {
|
||||
break;
|
||||
}
|
||||
|
||||
host = ObjectGetPrototypeOf(host);
|
||||
}
|
||||
|
||||
return descriptor;
|
||||
}
|
||||
|
||||
module.exports = { MockTracker };
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
const common = require('../common');
|
||||
const assert = require('node:assert');
|
||||
const { mock, test } = require('node:test');
|
||||
|
||||
test('spies on a function', (t) => {
|
||||
const sum = t.mock.fn((arg1, arg2) => {
|
||||
return arg1 + arg2;
|
||||
@@ -319,6 +318,157 @@ test('spy functions can be bound', (t) => {
|
||||
assert.strictEqual(sum.bind(0)(2, 11), 13);
|
||||
});
|
||||
|
||||
test('mocks prototype methods on an instance', async (t) => {
|
||||
class Runner {
|
||||
async someTask(msg) {
|
||||
return Promise.resolve(msg);
|
||||
}
|
||||
|
||||
async method(msg) {
|
||||
await this.someTask(msg);
|
||||
return msg;
|
||||
}
|
||||
}
|
||||
const msg = 'ok';
|
||||
const obj = new Runner();
|
||||
assert.strictEqual(await obj.method(msg), msg);
|
||||
|
||||
t.mock.method(obj, obj.someTask.name);
|
||||
assert.strictEqual(obj.someTask.mock.calls.length, 0);
|
||||
|
||||
assert.strictEqual(await obj.method(msg), msg);
|
||||
|
||||
const call = obj.someTask.mock.calls[0];
|
||||
|
||||
assert.deepStrictEqual(call.arguments, [msg]);
|
||||
assert.strictEqual(await call.result, msg);
|
||||
assert.strictEqual(call.target, undefined);
|
||||
assert.strictEqual(call.this, obj);
|
||||
|
||||
const obj2 = new Runner();
|
||||
// Ensure that a brand new instance is not mocked
|
||||
assert.strictEqual(
|
||||
obj2.someTask.mock,
|
||||
undefined
|
||||
);
|
||||
|
||||
assert.strictEqual(obj.someTask.mock.restore(), undefined);
|
||||
assert.strictEqual(await obj.method(msg), msg);
|
||||
assert.strictEqual(obj.someTask.mock, undefined);
|
||||
assert.strictEqual(Runner.prototype.someTask.mock, undefined);
|
||||
});
|
||||
|
||||
test('spies on async static class methods', async (t) => {
|
||||
class Runner {
|
||||
static async someTask(msg) {
|
||||
return Promise.resolve(msg);
|
||||
}
|
||||
|
||||
static async method(msg) {
|
||||
await this.someTask(msg);
|
||||
return msg;
|
||||
}
|
||||
}
|
||||
const msg = 'ok';
|
||||
assert.strictEqual(await Runner.method(msg), msg);
|
||||
|
||||
t.mock.method(Runner, Runner.someTask.name);
|
||||
assert.strictEqual(Runner.someTask.mock.calls.length, 0);
|
||||
|
||||
assert.strictEqual(await Runner.method(msg), msg);
|
||||
|
||||
const call = Runner.someTask.mock.calls[0];
|
||||
|
||||
assert.deepStrictEqual(call.arguments, [msg]);
|
||||
assert.strictEqual(await call.result, msg);
|
||||
assert.strictEqual(call.target, undefined);
|
||||
assert.strictEqual(call.this, Runner);
|
||||
|
||||
assert.strictEqual(Runner.someTask.mock.restore(), undefined);
|
||||
assert.strictEqual(await Runner.method(msg), msg);
|
||||
assert.strictEqual(Runner.someTask.mock, undefined);
|
||||
assert.strictEqual(Runner.prototype.someTask, undefined);
|
||||
|
||||
});
|
||||
|
||||
test('given null to a mock.method it throws a invalid argument error', (t) => {
|
||||
assert.throws(() => t.mock.method(null, {}), { code: 'ERR_INVALID_ARG_TYPE' });
|
||||
});
|
||||
|
||||
test('it should throw given an inexistent property on a object instance', (t) => {
|
||||
assert.throws(() => t.mock.method({ abc: 0 }, 'non-existent'), {
|
||||
code: 'ERR_INVALID_ARG_VALUE'
|
||||
});
|
||||
});
|
||||
|
||||
test('spy functions can be used on classes inheritance', (t) => {
|
||||
// Makes sure that having a null-prototype doesn't throw our system off
|
||||
class A extends null {
|
||||
static someTask(msg) {
|
||||
return msg;
|
||||
}
|
||||
static method(msg) {
|
||||
return this.someTask(msg);
|
||||
}
|
||||
}
|
||||
class B extends A {}
|
||||
class C extends B {}
|
||||
|
||||
const msg = 'ok';
|
||||
assert.strictEqual(C.method(msg), msg);
|
||||
|
||||
t.mock.method(C, C.someTask.name);
|
||||
assert.strictEqual(C.someTask.mock.calls.length, 0);
|
||||
|
||||
assert.strictEqual(C.method(msg), msg);
|
||||
|
||||
const call = C.someTask.mock.calls[0];
|
||||
|
||||
assert.deepStrictEqual(call.arguments, [msg]);
|
||||
assert.strictEqual(call.result, msg);
|
||||
assert.strictEqual(call.target, undefined);
|
||||
assert.strictEqual(call.this, C);
|
||||
|
||||
assert.strictEqual(C.someTask.mock.restore(), undefined);
|
||||
assert.strictEqual(C.method(msg), msg);
|
||||
assert.strictEqual(C.someTask.mock, undefined);
|
||||
});
|
||||
|
||||
test('spy functions don\'t affect the prototype chain', (t) => {
|
||||
|
||||
class A {
|
||||
static someTask(msg) {
|
||||
return msg;
|
||||
}
|
||||
}
|
||||
class B extends A {}
|
||||
class C extends B {}
|
||||
|
||||
const msg = 'ok';
|
||||
|
||||
const ABeforeMockIsUnchanged = Object.getOwnPropertyDescriptor(A, A.someTask.name);
|
||||
const BBeforeMockIsUnchanged = Object.getOwnPropertyDescriptor(B, B.someTask.name);
|
||||
const CBeforeMockShouldNotHaveDesc = Object.getOwnPropertyDescriptor(C, C.someTask.name);
|
||||
|
||||
t.mock.method(C, C.someTask.name);
|
||||
C.someTask(msg);
|
||||
const BAfterMockIsUnchanged = Object.getOwnPropertyDescriptor(B, B.someTask.name);
|
||||
|
||||
const AAfterMockIsUnchanged = Object.getOwnPropertyDescriptor(A, A.someTask.name);
|
||||
const CAfterMockHasDescriptor = Object.getOwnPropertyDescriptor(C, C.someTask.name);
|
||||
|
||||
assert.strictEqual(CBeforeMockShouldNotHaveDesc, undefined);
|
||||
assert.ok(CAfterMockHasDescriptor);
|
||||
|
||||
assert.deepStrictEqual(ABeforeMockIsUnchanged, AAfterMockIsUnchanged);
|
||||
assert.strictEqual(BBeforeMockIsUnchanged, BAfterMockIsUnchanged);
|
||||
assert.strictEqual(BBeforeMockIsUnchanged, undefined);
|
||||
|
||||
assert.strictEqual(C.someTask.mock.restore(), undefined);
|
||||
const CAfterRestoreKeepsDescriptor = Object.getOwnPropertyDescriptor(C, C.someTask.name);
|
||||
assert.ok(CAfterRestoreKeepsDescriptor);
|
||||
});
|
||||
|
||||
test('mocked functions report thrown errors', (t) => {
|
||||
const testError = new Error('test error');
|
||||
const fn = t.mock.fn(() => {
|
||||
|
||||
Reference in New Issue
Block a user