From 66939bc407daa58aee0ea57fa2540c97e3eda5fb Mon Sep 17 00:00:00 2001 From: Erick Wendel Date: Wed, 30 Nov 2022 16:09:01 -0300 Subject: [PATCH] test_runner: ensure that mock.method works with class inheritance --- lib/internal/test_runner/mock.js | 80 ++++++++++++++++++++++++---- test/parallel/test-runner-mocking.js | 44 +++++++++++++++ 2 files changed, 115 insertions(+), 9 deletions(-) diff --git a/lib/internal/test_runner/mock.js b/lib/internal/test_runner/mock.js index a1607aa030e68f..017f3ba6b3e9ce 100644 --- a/lib/internal/test_runner/mock.js +++ b/lib/internal/test_runner/mock.js @@ -174,19 +174,27 @@ class MockTracker { return original; } - let descriptor = ObjectGetOwnPropertyDescriptor(object, methodName); - let original = getOriginalObject(descriptor); - // classes instances - if (typeof original !== 'function') { - descriptor = ObjectGetOwnPropertyDescriptor( - ObjectGetPrototypeOf(object), - methodName - ); + function findMethodOnPrototypeChain(instance, method, isDescriptor = false) { + const original = isDescriptor ? + instance : + ObjectGetOwnPropertyDescriptor(instance, method); + + if (original && !isDescriptor) { + return original; + } - original = getOriginalObject(descriptor); + const proto = ObjectGetPrototypeOf(instance); + const desc = ObjectGetOwnPropertyDescriptor(proto, method); + if (!desc && proto.name) { + return findMethodOnPrototypeChain(proto, method, true); + } + return desc; } + const descriptor = findMethodOnPrototypeChain(object, methodName); + const original = getOriginalObject(descriptor); + if (typeof original !== 'function') { throw new ERR_INVALID_ARG_VALUE( 'methodName', original, 'must be a method' @@ -220,6 +228,60 @@ class MockTracker { return mock; } + getter( + object, + methodName, + implementation = kDefaultFunction, + options = kEmptyObject + ) { + if (implementation !== null && typeof implementation === 'object') { + options = implementation; + implementation = kDefaultFunction; + } else { + validateObject(options, 'options'); + } + + const { getter = true } = options; + + if (getter === false) { + throw new ERR_INVALID_ARG_VALUE( + 'options.getter', getter, 'cannot be false' + ); + } + + return this.method(object, methodName, implementation, { + ...options, + getter, + }); + } + + setter( + object, + methodName, + implementation = kDefaultFunction, + options = kEmptyObject + ) { + if (implementation !== null && typeof implementation === 'object') { + options = implementation; + implementation = kDefaultFunction; + } else { + validateObject(options, 'options'); + } + + const { setter = true } = options; + + if (setter === false) { + throw new ERR_INVALID_ARG_VALUE( + 'options.setter', setter, 'cannot be false' + ); + } + + return this.method(object, methodName, implementation, { + ...options, + setter, + }); + } + reset() { this.restoreAll(); this.#mocks = []; diff --git a/test/parallel/test-runner-mocking.js b/test/parallel/test-runner-mocking.js index 32a8acae5679b6..7084ee612343dd 100644 --- a/test/parallel/test-runner-mocking.js +++ b/test/parallel/test-runner-mocking.js @@ -318,6 +318,7 @@ test('spy functions can be bound', (t) => { assert.strictEqual(sum.mock.restore(), undefined); assert.strictEqual(sum.bind(0)(2, 11), 13); }); + test('mocks prototype methods on an instance', async (t) => { class Runner { async someTask(msg) { @@ -345,6 +346,13 @@ test('mocks prototype methods on an instance', async (t) => { 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); @@ -381,6 +389,42 @@ test('spies on async static class methods', async (t) => { assert.strictEqual(Runner.someTask.mock, undefined); }); +test('given null to a mock.method it throws a invalid argument error', (t) => { + assert.throws(() => t.mock.method(null, {}), /ERR_INVALID_ARG_TYPE/); +}); + +test('spy functions can be used on classes inheritance', (t) => { + class A { + 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('mocked functions report thrown errors', (t) => { const testError = new Error('test error'); const fn = t.mock.fn(() => {