Skip to content

Commit

Permalink
test: fix mock.method to support class instances
Browse files Browse the repository at this point in the history
It fixes a problem when trying to spy a method
from a class instance or static functions
on a class instance

PR-URL: #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>
  • Loading branch information
ErickWendel authored and juanarbol committed Jan 24, 2023
1 parent 286acaa commit 55e4140
Show file tree
Hide file tree
Showing 2 changed files with 176 additions and 6 deletions.
30 changes: 25 additions & 5 deletions lib/internal/test_runner/mock.js
Expand Up @@ -6,6 +6,7 @@ const {
FunctionPrototypeCall,
ObjectDefineProperty,
ObjectGetOwnPropertyDescriptor,
ObjectGetPrototypeOf,
Proxy,
ReflectApply,
ReflectConstruct,
Expand Down Expand Up @@ -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;
Expand All @@ -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) {
Expand All @@ -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);
Expand All @@ -201,7 +204,7 @@ class MockTracker {
mockDescriptor.value = mock;
}

ObjectDefineProperty(object, methodName, mockDescriptor);
ObjectDefineProperty(objectOrFunction, methodName, mockDescriptor);

return mock;
}
Expand Down Expand Up @@ -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 };
152 changes: 151 additions & 1 deletion test/parallel/test-runner-mocking.js
Expand Up @@ -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;
Expand Down Expand Up @@ -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(() => {
Expand Down

0 comments on commit 55e4140

Please sign in to comment.