Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Commit 62bb80c

Browse filesBrowse files
idango10richardlau
authored andcommitted
test_runner: support object property mocking
PR-URL: #58438 Fixes: #58322 Reviewed-By: Colin Ihrig <cjihrig@gmail.com> Reviewed-By: Chemi Atlow <chemi@atlow.co.il>
1 parent a93a8b5 commit 62bb80c
Copy full SHA for 62bb80c

File tree

Expand file treeCollapse file tree

3 files changed

+501
-1
lines changed
Open diff view settings
Filter options
Expand file treeCollapse file tree

3 files changed

+501
-1
lines changed
Open diff view settings
Collapse file

‎doc/api/test.md‎

Copy file name to clipboardExpand all lines: doc/api/test.md
+119Lines changed: 119 additions & 0 deletions
  • Display the source diff
  • Display the rich diff
Original file line numberDiff line numberDiff line change
@@ -1971,6 +1971,87 @@ added: v22.3.0
19711971

19721972
Resets the implementation of the mock module.
19731973

1974+
## Class: `MockPropertyContext`
1975+
1976+
<!-- YAML
1977+
added: REPLACEME
1978+
-->
1979+
1980+
The `MockPropertyContext` class is used to inspect or manipulate the behavior
1981+
of property mocks created via the [`MockTracker`][] APIs.
1982+
1983+
### `ctx.accesses`
1984+
1985+
* {Array}
1986+
1987+
A getter that returns a copy of the internal array used to track accesses (get/set) to
1988+
the mocked property. Each entry in the array is an object with the following properties:
1989+
1990+
* `type` {string} Either `'get'` or `'set'`, indicating the type of access.
1991+
* `value` {any} The value that was read (for `'get'`) or written (for `'set'`).
1992+
* `stack` {Error} An `Error` object whose stack can be used to determine the
1993+
callsite of the mocked function invocation.
1994+
1995+
### `ctx.accessCount()`
1996+
1997+
* Returns: {integer} The number of times that the property was accessed (read or written).
1998+
1999+
This function returns the number of times that the property was accessed.
2000+
This function is more efficient than checking `ctx.accesses.length` because
2001+
`ctx.accesses` is a getter that creates a copy of the internal access tracking array.
2002+
2003+
### `ctx.mockImplementation(value)`
2004+
2005+
* `value` {any} The new value to be set as the mocked property value.
2006+
2007+
This function is used to change the value returned by the mocked property getter.
2008+
2009+
### `ctx.mockImplementationOnce(value[, onAccess])`
2010+
2011+
* `value` {any} The value to be used as the mock's
2012+
implementation for the invocation number specified by `onAccess`.
2013+
* `onAccess` {integer} The invocation number that will use `value`. If
2014+
the specified invocation has already occurred then an exception is thrown.
2015+
**Default:** The number of the next invocation.
2016+
2017+
This function is used to change the behavior of an existing mock for a single
2018+
invocation. Once invocation `onAccess` has occurred, the mock will revert to
2019+
whatever behavior it would have used had `mockImplementationOnce()` not been
2020+
called.
2021+
2022+
The following example creates a mock function using `t.mock.property()`, calls the
2023+
mock property, changes the mock implementation to a different value for the
2024+
next invocation, and then resumes its previous behavior.
2025+
2026+
```js
2027+
test('changes a mock behavior once', (t) => {
2028+
const obj = { foo: 1 };
2029+
2030+
const prop = t.mock.property(obj, 'foo', 5);
2031+
2032+
assert.strictEqual(obj.foo, 5);
2033+
prop.mock.mockImplementationOnce(25);
2034+
assert.strictEqual(obj.foo, 25);
2035+
assert.strictEqual(obj.foo, 5);
2036+
});
2037+
```
2038+
2039+
#### Caveat
2040+
2041+
For consistency with the rest of the mocking API, this function treats both property gets and sets
2042+
as accesses. If a property set occurs at the same access index, the "once" value will be consumed
2043+
by the set operation, and the mocked property value will be changed to the "once" value. This may
2044+
lead to unexpected behavior if you intend the "once" value to only be used for a get operation.
2045+
2046+
### `ctx.resetAccesses()`
2047+
2048+
Resets the access history of the mocked property.
2049+
2050+
### `ctx.restore()`
2051+
2052+
Resets the implementation of the mock property to its original behavior. The
2053+
mock can still be used after calling this function.
2054+
19742055
## Class: `MockTracker`
19752056

19762057
<!-- YAML
@@ -2173,6 +2254,43 @@ test('mocks a builtin module in both module systems', async (t) => {
21732254
});
21742255
```
21752256

2257+
### `mock.property(object, propertyName[, value])`
2258+
2259+
<!-- YAML
2260+
added: REPLACEME
2261+
-->
2262+
2263+
* `object` {Object} The object whose value is being mocked.
2264+
* `propertyName` {string|symbol} The identifier of the property on `object` to mock.
2265+
* `value` {any} An optional value used as the mock value
2266+
for `object[propertyName]`. **Default:** The original property value.
2267+
* Returns: {Proxy} A proxy to the mocked object. The mocked object contains a
2268+
special `mock` property, which is an instance of [`MockPropertyContext`][], and
2269+
can be used for inspecting and changing the behavior of the mocked property.
2270+
2271+
Creates a mock for a property value on an object. This allows you to track and control access to a specific property,
2272+
including how many times it is read (getter) or written (setter), and to restore the original value after mocking.
2273+
2274+
```js
2275+
test('mocks a property value', (t) => {
2276+
const obj = { foo: 42 };
2277+
const prop = t.mock.property(obj, 'foo', 100);
2278+
2279+
assert.strictEqual(obj.foo, 100);
2280+
assert.strictEqual(prop.mock.accessCount(), 1);
2281+
assert.strictEqual(prop.mock.accesses[0].type, 'get');
2282+
assert.strictEqual(prop.mock.accesses[0].value, 100);
2283+
2284+
obj.foo = 200;
2285+
assert.strictEqual(prop.mock.accessCount(), 2);
2286+
assert.strictEqual(prop.mock.accesses[1].type, 'set');
2287+
assert.strictEqual(prop.mock.accesses[1].value, 200);
2288+
2289+
prop.mock.restore();
2290+
assert.strictEqual(obj.foo, 42);
2291+
});
2292+
```
2293+
21762294
### `mock.reset()`
21772295

21782296
<!-- YAML
@@ -3703,6 +3821,7 @@ Can be used to abort test subtasks when the test has been aborted.
37033821
[`--test-update-snapshots`]: cli.md#--test-update-snapshots
37043822
[`--test`]: cli.md#--test
37053823
[`MockFunctionContext`]: #class-mockfunctioncontext
3824+
[`MockPropertyContext`]: #class-mockpropertycontext
37063825
[`MockTimers`]: #class-mocktimers
37073826
[`MockTracker.method`]: #mockmethodobject-methodname-implementation-options
37083827
[`MockTracker`]: #class-mocktracker
Collapse file

‎lib/internal/test_runner/mock/mock.js‎

Copy file name to clipboardExpand all lines: lib/internal/test_runner/mock/mock.js
+163Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -284,6 +284,134 @@ class MockModuleContext {
284284

285285
const { restore: restoreModule } = MockModuleContext.prototype;
286286

287+
class MockPropertyContext {
288+
#object;
289+
#propertyName;
290+
#value;
291+
#originalValue;
292+
#descriptor;
293+
#accesses;
294+
#onceValues;
295+
296+
constructor(object, propertyName, value) {
297+
this.#onceValues = new SafeMap();
298+
this.#accesses = [];
299+
this.#object = object;
300+
this.#propertyName = propertyName;
301+
this.#originalValue = object[propertyName];
302+
this.#value = arguments.length > 2 ? value : this.#originalValue;
303+
this.#descriptor = ObjectGetOwnPropertyDescriptor(object, propertyName);
304+
if (!this.#descriptor) {
305+
throw new ERR_INVALID_ARG_VALUE(
306+
'propertyName', propertyName, 'is not a property of the object',
307+
);
308+
}
309+
310+
const { configurable, enumerable } = this.#descriptor;
311+
ObjectDefineProperty(object, propertyName, {
312+
__proto__: null,
313+
configurable,
314+
enumerable,
315+
get: () => {
316+
const nextValue = this.#getAccessValue(this.#value);
317+
const access = {
318+
__proto__: null,
319+
type: 'get',
320+
value: nextValue,
321+
// eslint-disable-next-line no-restricted-syntax
322+
stack: new Error(),
323+
};
324+
ArrayPrototypePush(this.#accesses, access);
325+
return nextValue;
326+
},
327+
set: this.mockImplementation.bind(this),
328+
});
329+
}
330+
331+
/**
332+
* Gets an array of recorded accesses (get/set) to the property.
333+
* @returns {Array} An array of access records.
334+
*/
335+
get accesses() {
336+
return ArrayPrototypeSlice(this.#accesses, 0);
337+
}
338+
339+
/**
340+
* Retrieves the number of times the property was accessed (get or set).
341+
* @returns {number} The total number of accesses.
342+
*/
343+
accessCount() {
344+
return this.#accesses.length;
345+
}
346+
347+
/**
348+
* Sets a new value for the property.
349+
* @param {any} value - The new value to be set.
350+
* @throws {Error} If the property is not writable.
351+
*/
352+
mockImplementation(value) {
353+
if (!this.#descriptor.writable) {
354+
throw new ERR_INVALID_ARG_VALUE(
355+
'propertyName', this.#propertyName, 'cannot be set',
356+
);
357+
}
358+
const nextValue = this.#getAccessValue(value);
359+
const access = {
360+
__proto__: null,
361+
type: 'set',
362+
value: nextValue,
363+
// eslint-disable-next-line no-restricted-syntax
364+
stack: new Error(),
365+
};
366+
ArrayPrototypePush(this.#accesses, access);
367+
this.#value = nextValue;
368+
}
369+
370+
#getAccessValue(value) {
371+
const accessIndex = this.#accesses.length;
372+
let accessValue;
373+
if (this.#onceValues.has(accessIndex)) {
374+
accessValue = this.#onceValues.get(accessIndex);
375+
this.#onceValues.delete(accessIndex);
376+
} else {
377+
accessValue = value;
378+
}
379+
return accessValue;
380+
}
381+
382+
/**
383+
* Sets a value to be used only for the next access (get or set), or a specific access index.
384+
* @param {any} value - The value to be used once.
385+
* @param {number} [onAccess] - The access index to be replaced.
386+
*/
387+
mockImplementationOnce(value, onAccess) {
388+
const nextAccess = this.#accesses.length;
389+
const accessIndex = onAccess ?? nextAccess;
390+
validateInteger(accessIndex, 'onAccess', nextAccess);
391+
this.#onceValues.set(accessIndex, value);
392+
}
393+
394+
/**
395+
* Resets the recorded accesses to the property.
396+
*/
397+
resetAccesses() {
398+
this.#accesses = [];
399+
}
400+
401+
/**
402+
* Restores the original value of the property that was mocked.
403+
*/
404+
restore() {
405+
ObjectDefineProperty(this.#object, this.#propertyName, {
406+
__proto__: null,
407+
...this.#descriptor,
408+
value: this.#originalValue,
409+
});
410+
}
411+
}
412+
413+
const { restore: restoreProperty } = MockPropertyContext.prototype;
414+
287415
class MockTracker {
288416
#mocks = [];
289417
#timers;
@@ -573,6 +701,41 @@ class MockTracker {
573701
return ctx;
574702
}
575703

704+
/**
705+
* Creates a property tracker for a specified object.
706+
* @param {(object)} object - The object whose value is being tracked.
707+
* @param {string} propertyName - The identifier of the property on object to be tracked.
708+
* @param {any} value - An optional replacement value used as the mock value for object[valueName].
709+
* @returns {ProxyConstructor} The mock property tracker.
710+
*/
711+
property(
712+
object,
713+
propertyName,
714+
value,
715+
) {
716+
validateObject(object, 'object');
717+
validateStringOrSymbol(propertyName, 'propertyName');
718+
719+
const ctx = arguments.length > 2 ?
720+
new MockPropertyContext(object, propertyName, value) :
721+
new MockPropertyContext(object, propertyName);
722+
ArrayPrototypePush(this.#mocks, {
723+
__proto__: null,
724+
ctx,
725+
restore: restoreProperty,
726+
});
727+
728+
return new Proxy(object, {
729+
__proto__: null,
730+
get(target, property, receiver) {
731+
if (property === 'mock') {
732+
return ctx;
733+
}
734+
return ReflectGet(target, property, receiver);
735+
},
736+
});
737+
}
738+
576739
/**
577740
* Resets the mock tracker, restoring all mocks and clearing timers.
578741
*/

0 commit comments

Comments
0 (0)
Morty Proxy This is a proxified and sanitized view of the page, visit original site.