Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 41 additions & 0 deletions doc/api/test.md
Original file line number Diff line number Diff line change
Expand Up @@ -2240,6 +2240,47 @@ test('mocks a builtin module in both module systems', async (t) => {
});
```

### `mock.value(object, valueName, value)`

<!-- YAML
added: REPLACEME
-->

* `object` {Object} The object whose value is being mocked.
* `valueName` {string|symbol} The identifier of the value on object to mock.
* `value` {any} A value used as the mock value for `object[valueName]`.

Creates a mock for a property value on an object. This allows you to track and control access to a specific property,
including how many times it is read (getter) or written (setter), and to restore the original value after mocking.

```js
test('mocks a property value', (t) => {
const obj = { foo: 42 };
const valueMock = t.mock.value(obj, 'foo', 100);

assert.strictEqual(obj.foo, 100);
assert.strictEqual(valueMock.callGetterCount(), 1);

obj.foo = 200;
assert.strictEqual(obj.foo, 200);
assert.strictEqual(valueMock.callSetterCount(), 1);

valueMock.restore();
assert.strictEqual(obj.foo, 42);
});
```

The returned mock object provides the following methods:

* `callGetterCount()`: Retrieves the number of times the property was read.
* `callSetterCount()`: Retrieves the number of times the property was written.
* `callCount()`: Retrieves the total number of times the property was accessed (getter or setter).
* `mockValue(value)`: Sets the current mock value.
* `resetCallGetters()`: Resets only the getter call count.
* `resetCallSetters()`: Resets only the setter call count.
* `resetCalls()`: Resets both getter and setter call counts.
* `restore()`: Restores the original property value and descriptor.

### `mock.reset()`

<!-- YAML
Expand Down
133 changes: 132 additions & 1 deletion lib/internal/test_runner/mock/mock.js
Original file line number Diff line number Diff line change
Expand Up @@ -284,6 +284,113 @@ class MockModuleContext {

const { restore: restoreModule } = MockModuleContext.prototype;

class MockValueContext {
#object;
#valueName;
#value;
#originalValue;
#callGetterCount = 0;
#callSetterCount = 0;
#descriptor;

constructor(object, valueName, value) {
this.#object = object;
this.#valueName = valueName;
this.#value = value;
this.#originalValue = object[valueName];
this.#descriptor = ObjectGetOwnPropertyDescriptor(object, valueName);

const { configurable, enumerable, writable } = this.#descriptor;
ObjectDefineProperty(object, valueName, {
__proto__: null,
configurable: configurable,
enumerable: enumerable,
get: () => {
this.#callGetterCount++;
return this.#value;
},
set: writable ?
(newValue) => {
this.#callSetterCount++;
this.#value = newValue;
} :
() => {
throw new ERR_INVALID_ARG_VALUE(
'valueName', valueName, 'cannot be set',
);
},
});
}

/**
* Retrieves the number of times the property was read.
* @returns {number} The number of times the getter was called.
*/
callGetterCount() {
return this.#callGetterCount;
}

/**
* Retrieves the number of times the property was written.
* @returns {number} The number of times the setter was called.
*/
callSetterCount() {
return this.#callSetterCount;
}

/**
* Retrieves the total number of times the property was accessed (getter or setter)
* @returns {number} The total number of calls.
*/
callCount() {
return this.callGetterCount() + this.callSetterCount();
}

/**
* Sets a new value for the property.
* @param {any} value - The new value to be set.
* @throws {Error} If the property is not writable.
*/
mockValue(value) {
this.#value = value;
}

/**
* Resets the call count for the getter.
*/
resetCallGetters() {
this.#callGetterCount = 0;
}

/**
* Resets the call count for the setter.
*/
resetCallSetters() {
this.#callSetterCount = 0;
}

/**
* Resets the call counts for both getter and setter.
*/
resetCalls() {
this.resetCallGetters();
this.resetCallSetters();
}

/**
* Restores the original value of the property that was mocked.
*/
restore() {
ObjectDefineProperty(this.#object, this.#valueName, {
__proto__: null,
...this.#descriptor,
value: this.#originalValue,
});
}
}

const { restore: restoreValue } = MockValueContext.prototype;

class MockTracker {
#mocks = [];
#timers;
Expand Down Expand Up @@ -573,6 +680,30 @@ class MockTracker {
return ctx;
}

/**
* Creates a value tracker for a specified object.
* @param {(object)} object - The object whose value is being tracked.
* @param {string} valueName - The identifier of the value on object to be tracked.
* @param {any} value - A value used as the mock value for object[valueName].
* @returns {MockValueContext} The mock value tracker.
*/
value(
object,
valueName,
value,
) {
validateObject(object, 'object');
validateStringOrSymbol(valueName, 'valueName');

const ctx = new MockValueContext(object, valueName, value);
ArrayPrototypePush(this.#mocks, {
__proto__: null,
ctx,
restore: restoreValue,
});
return ctx;
}

/**
* Resets the mock tracker, restoring all mocks and clearing timers.
*/
Expand Down Expand Up @@ -726,7 +857,7 @@ function cjsMockModuleLoad(request, parent, isMain) {
const exportNames = ObjectKeys(namedExports);

if ((typeof modExports !== 'object' || modExports === null) &&
exportNames.length > 0) {
exportNames.length > 0) {
// eslint-disable-next-line no-restricted-syntax
throw new Error(kBadExportsMessage);
}
Expand Down
65 changes: 65 additions & 0 deletions test/parallel/test-runner-mocking.js
Original file line number Diff line number Diff line change
Expand Up @@ -1054,3 +1054,68 @@ test('setter() fails if getter options is true', (t) => {
t.mock.setter({}, 'method', { getter: true });
}, /The property 'options\.setter' cannot be used with 'options\.getter'/);
});

test('spies on a property value', (t) => {
const obj = { foo: 42 };
const valueMock = t.mock.value(obj, 'foo', 100);

assert.strictEqual(obj.foo, 100);
assert.strictEqual(valueMock.callCount(), 1);
assert.strictEqual(valueMock.callGetterCount(), 1);
assert.strictEqual(valueMock.callSetterCount(), 0);

obj.foo = 200;
assert.strictEqual(obj.foo, 200);
assert.strictEqual(valueMock.callCount(), 3);
assert.strictEqual(valueMock.callGetterCount(), 2);
assert.strictEqual(valueMock.callSetterCount(), 1);

obj.foo = 300;
assert.strictEqual(obj.foo, 300);
assert.strictEqual(valueMock.callSetterCount(), 2);

valueMock.mockValue(400);
assert.strictEqual(obj.foo, 400);
assert.strictEqual(valueMock.callGetterCount(), 4);

valueMock.resetCalls();
assert.strictEqual(valueMock.callCount(), 0);
assert.strictEqual(valueMock.callGetterCount(), 0);
assert.strictEqual(valueMock.callSetterCount(), 0);

obj.foo = 500;
assert.strictEqual(valueMock.callSetterCount(), 1);

valueMock.resetCallSetters();
assert.strictEqual(valueMock.callSetterCount(), 0);

assert.strictEqual(obj.foo, 500);
assert.strictEqual(valueMock.callGetterCount(), 1);

valueMock.resetCallGetters();
assert.strictEqual(valueMock.callGetterCount(), 0);

valueMock.restore();
assert.strictEqual(obj.foo, 42);

obj.foo = 600;
assert.strictEqual(obj.foo, 600);
assert.strictEqual(valueMock.callCount(), 0);
});

test('spies on a non-writable property value', (t) => {
const obj = {};
Object.defineProperty(obj, 'bar', {
value: 1,
writable: false,
configurable: true,
enumerable: true,
});

const valueMock = t.mock.value(obj, 'bar', 2);
assert.strictEqual(obj.bar, 2);
assert.throws(() => { obj.bar = 3; }, /cannot be set/);

valueMock.restore();
assert.strictEqual(obj.bar, 1);
});
Loading