mirror of https://github.com/facebook/jest.git
feat: Allow mocking property value in tests (#13496)
This commit is contained in:
parent
a325f8790d
commit
e77128b128
|
@ -2,6 +2,7 @@
|
|||
|
||||
### Features
|
||||
|
||||
- `[@jest/globals, jest-mock]` Add `jest.replaceProperty()` that replaces property value ([#13496](https://github.com/facebook/jest/pull/13496))
|
||||
- `[expect, @jest/expect-utils]` Support custom equality testers ([#13654](https://github.com/facebook/jest/pull/13654))
|
||||
- `[jest-haste-map]` ignore Sapling vcs directories (`.sl/`) ([#13674](https://github.com/facebook/jest/pull/13674))
|
||||
- `[jest-resolve]` Support subpath imports ([#13705](https://github.com/facebook/jest/pull/13705))
|
||||
|
|
|
@ -608,13 +608,62 @@ See [Mock Functions](MockFunctionAPI.md#jestfnimplementation) page for details o
|
|||
|
||||
Determines if the given function is a mocked function.
|
||||
|
||||
### `jest.replaceProperty(object, propertyKey, value)`
|
||||
|
||||
Replace `object[propertyKey]` with a `value`. The property must already exist on the object. The same property might be replaced multiple times. Returns a Jest [replaced property](MockFunctionAPI.md#replaced-properties).
|
||||
|
||||
:::note
|
||||
|
||||
To mock properties that are defined as getters or setters, use [`jest.spyOn(object, methodName, accessType)`](#jestspyonobject-methodname-accesstype) instead. To mock functions, use [`jest.spyOn(object, methodName)`](#jestspyonobject-methodname) instead.
|
||||
|
||||
:::
|
||||
|
||||
:::tip
|
||||
|
||||
All properties replaced with `jest.replaceProperty` could be restored to the original value by calling [jest.restoreAllMocks](#jestrestoreallmocks) on [afterEach](GlobalAPI.md#aftereachfn-timeout) method.
|
||||
|
||||
:::
|
||||
|
||||
Example:
|
||||
|
||||
```js
|
||||
const utils = {
|
||||
isLocalhost() {
|
||||
return process.env.HOSTNAME === 'localhost';
|
||||
},
|
||||
};
|
||||
|
||||
module.exports = utils;
|
||||
```
|
||||
|
||||
Example test:
|
||||
|
||||
```js
|
||||
const utils = require('./utils');
|
||||
|
||||
afterEach(() => {
|
||||
// restore replaced property
|
||||
jest.restoreAllMocks();
|
||||
});
|
||||
|
||||
test('isLocalhost returns true when HOSTNAME is localhost', () => {
|
||||
jest.replaceProperty(process, 'env', {HOSTNAME: 'localhost'});
|
||||
expect(utils.isLocalhost()).toBe(true);
|
||||
});
|
||||
|
||||
test('isLocalhost returns false when HOSTNAME is not localhost', () => {
|
||||
jest.replaceProperty(process, 'env', {HOSTNAME: 'not-localhost'});
|
||||
expect(utils.isLocalhost()).toBe(false);
|
||||
});
|
||||
```
|
||||
|
||||
### `jest.spyOn(object, methodName)`
|
||||
|
||||
Creates a mock function similar to `jest.fn` but also tracks calls to `object[methodName]`. Returns a Jest [mock function](MockFunctionAPI.md).
|
||||
|
||||
:::note
|
||||
|
||||
By default, `jest.spyOn` also calls the **spied** method. This is different behavior from most other test libraries. If you want to overwrite the original function, you can use `jest.spyOn(object, methodName).mockImplementation(() => customImplementation)` or `object[methodName] = jest.fn(() => customImplementation);`
|
||||
By default, `jest.spyOn` also calls the **spied** method. This is different behavior from most other test libraries. If you want to overwrite the original function, you can use `jest.spyOn(object, methodName).mockImplementation(() => customImplementation)` or `jest.replaceProperty(object, methodName, jest.fn(() => customImplementation));`
|
||||
|
||||
:::
|
||||
|
||||
|
@ -713,6 +762,10 @@ test('plays audio', () => {
|
|||
});
|
||||
```
|
||||
|
||||
### `jest.Replaced<Source>`
|
||||
|
||||
See [TypeScript Usage](MockFunctionAPI.md#replacedpropertyreplacevaluevalue) chapter of Mock Functions page for documentation.
|
||||
|
||||
### `jest.Spied<Source>`
|
||||
|
||||
See [TypeScript Usage](MockFunctionAPI.md#jestspiedsource) chapter of Mock Functions page for documentation.
|
||||
|
@ -731,7 +784,7 @@ Returns the `jest` object for chaining.
|
|||
|
||||
### `jest.restoreAllMocks()`
|
||||
|
||||
Restores all mocks back to their original value. Equivalent to calling [`.mockRestore()`](MockFunctionAPI.md#mockfnmockrestore) on every mocked function. Beware that `jest.restoreAllMocks()` only works when the mock was created with `jest.spyOn`; other mocks will require you to manually restore them.
|
||||
Restores all mocks and replaced properties back to their original value. Equivalent to calling [`.mockRestore()`](MockFunctionAPI.md#mockfnmockrestore) on every mocked function and [`.restore()`](MockFunctionAPI.md#replacedpropertyrestore) on every replaced property. Beware that `jest.restoreAllMocks()` only works for mocks created with [`jest.spyOn()`](#jestspyonobject-methodname) and properties replaced with [`jest.replaceProperty()`](#jestreplacepropertyobject-propertykey-value); other mocks will require you to manually restore them.
|
||||
|
||||
## Fake Timers
|
||||
|
||||
|
|
|
@ -515,6 +515,20 @@ test('async test', async () => {
|
|||
});
|
||||
```
|
||||
|
||||
## Replaced Properties
|
||||
|
||||
### `replacedProperty.replaceValue(value)`
|
||||
|
||||
Changes the value of already replaced property. This is useful when you want to replace property and then adjust the value in specific tests. As an alternative, you can call [`jest.replaceProperty()`](JestObjectAPI.md#jestreplacepropertyobject-propertykey-value) multiple times on same property.
|
||||
|
||||
### `replacedProperty.restore()`
|
||||
|
||||
Restores object's property to the original value.
|
||||
|
||||
Beware that `replacedProperty.restore()` only works when the property value was replaced with [`jest.replaceProperty()`](JestObjectAPI.md#jestreplacepropertyobject-propertykey-value).
|
||||
|
||||
The [`restoreMocks`](configuration#restoremocks-boolean) configuration option is available to restore replaced properties automatically before each test.
|
||||
|
||||
## TypeScript Usage
|
||||
|
||||
<TypeScriptExamplesNote />
|
||||
|
@ -594,6 +608,39 @@ test('returns correct data', () => {
|
|||
|
||||
Types of classes, functions or objects can be passed as type argument to `jest.Mocked<Source>`. If you prefer to constrain the input type, use: `jest.MockedClass<Source>`, `jest.MockedFunction<Source>` or `jest.MockedObject<Source>`.
|
||||
|
||||
### `jest.Replaced<Source>`
|
||||
|
||||
The `jest.Replaced<Source>` utility type returns the `Source` type wrapped with type definitions of Jest [replaced property](#replaced-properties).
|
||||
|
||||
```ts title="src/utils.ts"
|
||||
export function isLocalhost(): boolean {
|
||||
return process.env['HOSTNAME'] === 'localhost';
|
||||
}
|
||||
```
|
||||
|
||||
```ts title="src/__tests__/utils.test.ts"
|
||||
import {afterEach, expect, it, jest} from '@jest/globals';
|
||||
import {isLocalhost} from '../utils';
|
||||
|
||||
let replacedEnv: jest.Replaced<typeof process.env> | undefined = undefined;
|
||||
|
||||
afterEach(() => {
|
||||
replacedEnv?.restore();
|
||||
});
|
||||
|
||||
it('isLocalhost should detect localhost environment', () => {
|
||||
replacedEnv = jest.replaceProperty(process, 'env', {HOSTNAME: 'localhost'});
|
||||
|
||||
expect(isLocalhost()).toBe(true);
|
||||
});
|
||||
|
||||
it('isLocalhost should detect non-localhost environment', () => {
|
||||
replacedEnv = jest.replaceProperty(process, 'env', {HOSTNAME: 'example.com'});
|
||||
|
||||
expect(isLocalhost()).toBe(false);
|
||||
});
|
||||
```
|
||||
|
||||
### `jest.mocked(source, options?)`
|
||||
|
||||
The `mocked()` helper method wraps types of the `source` object and its deep nested members with type definitions of Jest mock function. You can pass `{shallow: true}` as the `options` argument to disable the deeply mocked behavior.
|
||||
|
|
|
@ -0,0 +1,17 @@
|
|||
import {isLocalhost} from '../utils';
|
||||
|
||||
afterEach(() => {
|
||||
jest.restoreAllMocks();
|
||||
});
|
||||
|
||||
it('isLocalhost should detect localhost environment', () => {
|
||||
jest.replaceProperty(process, 'env', {HOSTNAME: 'localhost'});
|
||||
|
||||
expect(isLocalhost()).toBe(true);
|
||||
});
|
||||
|
||||
it('isLocalhost should detect non-localhost environment', () => {
|
||||
jest.replaceProperty(process, 'env', {HOSTNAME: 'example.com'});
|
||||
|
||||
expect(isLocalhost()).toBe(false);
|
||||
});
|
|
@ -0,0 +1,3 @@
|
|||
export function isLocalhost() {
|
||||
return process.env.HOSTNAME === 'localhost';
|
||||
}
|
|
@ -0,0 +1,24 @@
|
|||
// Copyright (c) 2014-present, Facebook, Inc. All rights reserved.
|
||||
|
||||
import {afterEach, beforeEach, expect, it, jest} from '@jest/globals';
|
||||
import {isLocalhost} from '../utils';
|
||||
|
||||
let replacedEnv: jest.Replaced<typeof process.env> | undefined = undefined;
|
||||
|
||||
beforeEach(() => {
|
||||
replacedEnv = jest.replaceProperty(process, 'env', {});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
replacedEnv?.restore();
|
||||
});
|
||||
|
||||
it('isLocalhost should detect localhost environment', () => {
|
||||
replacedEnv.replaceValue({HOSTNAME: 'localhost'});
|
||||
|
||||
expect(isLocalhost()).toBe(true);
|
||||
});
|
||||
|
||||
it('isLocalhost should detect non-localhost environment', () => {
|
||||
expect(isLocalhost()).toBe(false);
|
||||
});
|
|
@ -0,0 +1,5 @@
|
|||
// Copyright (c) 2014-present, Facebook, Inc. All rights reserved.
|
||||
|
||||
export function isLocalhost() {
|
||||
return process.env.HOSTNAME === 'localhost';
|
||||
}
|
|
@ -223,6 +223,13 @@ export interface Jest {
|
|||
* mocked behavior.
|
||||
*/
|
||||
mocked: ModuleMocker['mocked'];
|
||||
/**
|
||||
* Replaces property on an object with another value.
|
||||
*
|
||||
* @remarks
|
||||
* For mocking functions or 'get' or 'set' accessors, use `jest.spyOn()` instead.
|
||||
*/
|
||||
replaceProperty: ModuleMocker['replaceProperty'];
|
||||
/**
|
||||
* Returns a mock module instead of the actual module, bypassing all checks
|
||||
* on whether the module should be required normally or not.
|
||||
|
@ -239,8 +246,9 @@ export interface Jest {
|
|||
*/
|
||||
resetModules(): Jest;
|
||||
/**
|
||||
* Restores all mocks back to their original value. Equivalent to calling
|
||||
* `.mockRestore()` on every mocked function.
|
||||
* Restores all mocks and replaced properties back to their original value.
|
||||
* Equivalent to calling `.mockRestore()` on every mocked function
|
||||
* and `.restore()` on every replaced property.
|
||||
*
|
||||
* Beware that `jest.restoreAllMocks()` only works when the mock was created
|
||||
* with `jest.spyOn()`; other mocks will require you to manually restore them.
|
||||
|
|
|
@ -16,6 +16,7 @@ import type {
|
|||
MockedClass as JestMockedClass,
|
||||
MockedFunction as JestMockedFunction,
|
||||
MockedObject as JestMockedObject,
|
||||
Replaced as JestReplaced,
|
||||
Spied as JestSpied,
|
||||
SpiedClass as JestSpiedClass,
|
||||
SpiedFunction as JestSpiedFunction,
|
||||
|
@ -63,6 +64,10 @@ declare namespace jest {
|
|||
* Wraps an object type with Jest mock type definitions.
|
||||
*/
|
||||
export type MockedObject<T extends object> = JestMockedObject<T>;
|
||||
/**
|
||||
* Constructs the type of a replaced property.
|
||||
*/
|
||||
export type Replaced<T> = JestReplaced<T>;
|
||||
/**
|
||||
* Constructs the type of a spied class or function.
|
||||
*/
|
||||
|
|
|
@ -15,11 +15,13 @@ import {
|
|||
} from 'tsd-lite';
|
||||
import {
|
||||
Mock,
|
||||
Replaced,
|
||||
SpiedClass,
|
||||
SpiedFunction,
|
||||
SpiedGetter,
|
||||
SpiedSetter,
|
||||
fn,
|
||||
replaceProperty,
|
||||
spyOn,
|
||||
} from 'jest-mock';
|
||||
|
||||
|
@ -492,3 +494,71 @@ expectError(
|
|||
(key: string, value: number) => {},
|
||||
),
|
||||
);
|
||||
|
||||
// replaceProperty + Replaced
|
||||
|
||||
const obj = {
|
||||
fn: () => {},
|
||||
|
||||
property: 1,
|
||||
};
|
||||
|
||||
expectType<Replaced<number>>(replaceProperty(obj, 'property', 1));
|
||||
expectType<void>(replaceProperty(obj, 'property', 1).replaceValue(1).restore());
|
||||
|
||||
expectError(replaceProperty(obj, 'invalid', 1));
|
||||
expectError(replaceProperty(obj, 'property', 'not a number'));
|
||||
expectError(replaceProperty(obj, 'fn', () => {}));
|
||||
|
||||
expectError(replaceProperty(obj, 'property', 1).replaceValue('not a number'));
|
||||
|
||||
interface ComplexObject {
|
||||
numberOrUndefined: number | undefined;
|
||||
optionalString?: string;
|
||||
multipleTypes: number | string | {foo: number} | null;
|
||||
}
|
||||
declare const complexObject: ComplexObject;
|
||||
|
||||
interface ObjectWithDynamicProperties {
|
||||
[key: string]: boolean;
|
||||
}
|
||||
declare const objectWithDynamicProperties: ObjectWithDynamicProperties;
|
||||
|
||||
// Resulting type should retain the original property type
|
||||
expectType<Replaced<number | undefined>>(
|
||||
replaceProperty(complexObject, 'numberOrUndefined', undefined),
|
||||
);
|
||||
expectType<Replaced<number | undefined>>(
|
||||
replaceProperty(complexObject, 'numberOrUndefined', 1),
|
||||
);
|
||||
|
||||
expectError(
|
||||
replaceProperty(
|
||||
complexObject,
|
||||
'numberOrUndefined',
|
||||
'string is not valid TypeScript type',
|
||||
),
|
||||
);
|
||||
|
||||
expectType<Replaced<string | undefined>>(
|
||||
replaceProperty(complexObject, 'optionalString', 'foo'),
|
||||
);
|
||||
expectType<Replaced<string | undefined>>(
|
||||
replaceProperty(complexObject, 'optionalString', undefined),
|
||||
);
|
||||
|
||||
expectType<Replaced<boolean>>(
|
||||
replaceProperty(objectWithDynamicProperties, 'dynamic prop 1', true),
|
||||
);
|
||||
expectError(
|
||||
replaceProperty(objectWithDynamicProperties, 'dynamic prop 1', undefined),
|
||||
);
|
||||
|
||||
expectError(replaceProperty(complexObject, 'not a property', undefined));
|
||||
|
||||
expectType<Replaced<ComplexObject['multipleTypes']>>(
|
||||
replaceProperty(complexObject, 'multipleTypes', 1)
|
||||
.replaceValue('foo')
|
||||
.replaceValue({foo: 1})
|
||||
.replaceValue(null),
|
||||
);
|
||||
|
|
|
@ -1279,12 +1279,12 @@ describe('moduleMocker', () => {
|
|||
expect(() => {
|
||||
moduleMocker.spyOn({}, 'method');
|
||||
}).toThrow(
|
||||
'Cannot spy the method property because it is not a function; undefined given instead',
|
||||
"Cannot spy the method property because it is not a function; undefined given instead. If you are trying to mock a property, use `jest.replaceProperty(object, 'method', value)` instead.",
|
||||
);
|
||||
expect(() => {
|
||||
moduleMocker.spyOn({method: 10}, 'method');
|
||||
}).toThrow(
|
||||
'Cannot spy the method property because it is not a function; number given instead',
|
||||
"Cannot spy the method property because it is not a function; number given instead. If you are trying to mock a property, use `jest.replaceProperty(object, 'method', value)` instead.",
|
||||
);
|
||||
});
|
||||
|
||||
|
@ -1449,12 +1449,12 @@ describe('moduleMocker', () => {
|
|||
expect(() => {
|
||||
moduleMocker.spyOn({}, 'method');
|
||||
}).toThrow(
|
||||
'Cannot spy the method property because it is not a function; undefined given instead',
|
||||
"Cannot spy the method property because it is not a function; undefined given instead. If you are trying to mock a property, use `jest.replaceProperty(object, 'method', value)` instead.",
|
||||
);
|
||||
expect(() => {
|
||||
moduleMocker.spyOn({method: 10}, 'method');
|
||||
}).toThrow(
|
||||
'Cannot spy the method property because it is not a function; number given instead',
|
||||
"Cannot spy the method property because it is not a function; number given instead. If you are trying to mock a property, use `jest.replaceProperty(object, 'method', value)` instead.",
|
||||
);
|
||||
});
|
||||
|
||||
|
@ -1604,6 +1604,236 @@ describe('moduleMocker', () => {
|
|||
expect(spy2.mock.calls).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('replaceProperty', () => {
|
||||
it('should work', () => {
|
||||
const obj = {
|
||||
property: 1,
|
||||
};
|
||||
|
||||
const replaced = moduleMocker.replaceProperty(obj, 'property', 2);
|
||||
|
||||
expect(obj.property).toBe(2);
|
||||
|
||||
replaced.restore();
|
||||
|
||||
expect(obj.property).toBe(1);
|
||||
});
|
||||
|
||||
it('should allow mocking a property multiple times', () => {
|
||||
const obj = {
|
||||
property: 1,
|
||||
};
|
||||
|
||||
const replacedFirst = moduleMocker.replaceProperty(obj, 'property', 2);
|
||||
|
||||
const replacedSecond = moduleMocker.replaceProperty(obj, 'property', 3);
|
||||
|
||||
expect(obj.property).toBe(3);
|
||||
|
||||
replacedSecond.restore();
|
||||
|
||||
expect(obj.property).toBe(1);
|
||||
|
||||
replacedFirst.restore();
|
||||
|
||||
expect(obj.property).toBe(1);
|
||||
});
|
||||
|
||||
it('should allow mocking with value of different value', () => {
|
||||
const obj = {
|
||||
property: 1,
|
||||
};
|
||||
|
||||
const replaced = moduleMocker.replaceProperty(obj, 'property', {
|
||||
foo: 'bar',
|
||||
});
|
||||
|
||||
expect(obj.property).toStrictEqual({foo: 'bar'});
|
||||
|
||||
replaced.restore();
|
||||
|
||||
expect(obj.property).toBe(1);
|
||||
});
|
||||
|
||||
describe('should throw', () => {
|
||||
it.each`
|
||||
value
|
||||
${null}
|
||||
${undefined}
|
||||
`('when $value is provided instead of an object', ({value}) => {
|
||||
expect(() => {
|
||||
moduleMocker.replaceProperty(value, 'property', 1);
|
||||
}).toThrow(
|
||||
'replaceProperty could not find an object on which to replace property',
|
||||
);
|
||||
});
|
||||
|
||||
it.each`
|
||||
value | type
|
||||
${'foo'} | ${'string'}
|
||||
${1} | ${'number'}
|
||||
${NaN} | ${'number'}
|
||||
${1n} | ${'bigint'}
|
||||
${Symbol()} | ${'symbol'}
|
||||
${true} | ${'boolean'}
|
||||
${false} | ${'boolean'}
|
||||
${() => {}} | ${'function'}
|
||||
`(
|
||||
'when primitive value $value is provided instead of an object',
|
||||
({value, type}) => {
|
||||
expect(() => {
|
||||
moduleMocker.replaceProperty(value, 'property', 1);
|
||||
}).toThrow(
|
||||
`Cannot mock property on a non-object value; ${type} given`,
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
it('when property name is not provided', () => {
|
||||
expect(() => {
|
||||
moduleMocker.replaceProperty({}, null, 1);
|
||||
}).toThrow('No property name supplied');
|
||||
});
|
||||
|
||||
it('when property is not defined', () => {
|
||||
expect(() => {
|
||||
moduleMocker.replaceProperty({}, 'doesNotExist', 1);
|
||||
}).toThrow('doesNotExist property does not exist');
|
||||
});
|
||||
|
||||
it('when property is not configurable', () => {
|
||||
expect(() => {
|
||||
const obj = {};
|
||||
|
||||
Object.defineProperty(obj, 'property', {
|
||||
configurable: false,
|
||||
value: 1,
|
||||
writable: false,
|
||||
});
|
||||
|
||||
moduleMocker.replaceProperty(obj, 'property', 2);
|
||||
}).toThrow('property is not declared configurable');
|
||||
});
|
||||
|
||||
it('when trying to mock a method', () => {
|
||||
expect(() => {
|
||||
moduleMocker.replaceProperty({method: () => {}}, 'method', () => {});
|
||||
}).toThrow(
|
||||
"Cannot mock the method property because it is a function. Use `jest.spyOn(object, 'method')` instead.",
|
||||
);
|
||||
});
|
||||
|
||||
it('when mocking a getter', () => {
|
||||
const obj = {
|
||||
get getter() {
|
||||
return 1;
|
||||
},
|
||||
};
|
||||
|
||||
expect(() => {
|
||||
moduleMocker.replaceProperty(obj, 'getter', 1);
|
||||
}).toThrow('Cannot mock the getter property because it has a getter');
|
||||
});
|
||||
|
||||
it('when mocking a setter', () => {
|
||||
const obj = {
|
||||
// eslint-disable-next-line accessor-pairs
|
||||
set setter(_value: number) {},
|
||||
};
|
||||
|
||||
expect(() => {
|
||||
moduleMocker.replaceProperty(obj, 'setter', 1);
|
||||
}).toThrow('Cannot mock the setter property because it has a setter');
|
||||
});
|
||||
});
|
||||
|
||||
it('should work for property from prototype chain', () => {
|
||||
const parent = {property: 'abcd'};
|
||||
const child = Object.create(parent);
|
||||
|
||||
const replaced = moduleMocker.replaceProperty(child, 'property', 'defg');
|
||||
|
||||
expect(child.property).toBe('defg');
|
||||
|
||||
replaced.restore();
|
||||
|
||||
expect(child.property).toBe('abcd');
|
||||
expect(
|
||||
Object.getOwnPropertyDescriptor(child, 'property'),
|
||||
).toBeUndefined();
|
||||
});
|
||||
|
||||
describe('with restoreAllMocks', () => {
|
||||
it('should work', () => {
|
||||
const obj = {
|
||||
property: 1,
|
||||
};
|
||||
|
||||
const replaced = moduleMocker.replaceProperty(obj, 'property', 2);
|
||||
|
||||
expect(obj.property).toBe(2);
|
||||
|
||||
moduleMocker.restoreAllMocks();
|
||||
|
||||
expect(obj.property).toBe(1);
|
||||
|
||||
// Just make sure that this call won't break anything while calling after the property has been already restored
|
||||
replaced.restore();
|
||||
|
||||
expect(obj.property).toBe(1);
|
||||
});
|
||||
|
||||
it('should work for property mocked multiple times', () => {
|
||||
const obj = {
|
||||
property: 1,
|
||||
};
|
||||
|
||||
const replaced1 = moduleMocker.replaceProperty(obj, 'property', 2);
|
||||
const replaced2 = moduleMocker.replaceProperty(obj, 'property', 3);
|
||||
|
||||
expect(obj.property).toBe(3);
|
||||
|
||||
moduleMocker.restoreAllMocks();
|
||||
|
||||
expect(obj.property).toBe(1);
|
||||
|
||||
// Just make sure that this call won't break anything while calling after the property has been already restored
|
||||
replaced2.restore();
|
||||
replaced1.restore();
|
||||
|
||||
expect(obj.property).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('replaceValue', () => {
|
||||
it('should work', () => {
|
||||
const obj = {
|
||||
property: 1,
|
||||
};
|
||||
|
||||
const replaced = moduleMocker.replaceProperty(obj, 'property', 2);
|
||||
|
||||
const result = replaced.replaceValue(3);
|
||||
|
||||
expect(obj.property).toBe(3);
|
||||
expect(result).toBe(replaced);
|
||||
});
|
||||
|
||||
it('should work while passing different type', () => {
|
||||
const obj = {
|
||||
property: 1,
|
||||
};
|
||||
|
||||
const replaced = moduleMocker.replaceProperty(obj, 'property', 2);
|
||||
|
||||
const result = replaced.replaceValue('foo');
|
||||
|
||||
expect(obj.property).toBe('foo');
|
||||
expect(result).toBe(replaced);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('mocked', () => {
|
||||
|
|
|
@ -172,6 +172,28 @@ export interface MockInstance<T extends FunctionLike = UnknownFunction> {
|
|||
mockRejectedValueOnce(value: RejectType<T>): this;
|
||||
}
|
||||
|
||||
export interface Replaced<T = unknown> {
|
||||
/**
|
||||
* Restore property to its original value known at the time of mocking.
|
||||
*/
|
||||
restore(): void;
|
||||
|
||||
/**
|
||||
* Change the value of the property.
|
||||
*/
|
||||
replaceValue(value: T): this;
|
||||
}
|
||||
|
||||
type ReplacedPropertyRestorer<
|
||||
T extends object,
|
||||
K extends PropertyLikeKeys<T>,
|
||||
> = {
|
||||
(): void;
|
||||
object: T;
|
||||
property: K;
|
||||
replaced: Replaced<T[K]>;
|
||||
};
|
||||
|
||||
type MockFunctionResultIncomplete = {
|
||||
type: 'incomplete';
|
||||
/**
|
||||
|
@ -956,6 +978,27 @@ export class ModuleMocker {
|
|||
return mock as Mocked<T>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether the given property of an object has been already replaced.
|
||||
*/
|
||||
private _findReplacedProperty<
|
||||
T extends object,
|
||||
K extends PropertyLikeKeys<T>,
|
||||
>(object: T, propertyKey: K): ReplacedPropertyRestorer<T, K> | undefined {
|
||||
for (const spyState of this._spyState) {
|
||||
if (
|
||||
'object' in spyState &&
|
||||
'property' in spyState &&
|
||||
spyState.object === object &&
|
||||
spyState.property === propertyKey
|
||||
) {
|
||||
return spyState as ReplacedPropertyRestorer<T, K>;
|
||||
}
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* @see README.md
|
||||
* @param metadata Metadata for the mock in the schema returned by the
|
||||
|
@ -1123,7 +1166,13 @@ export class ModuleMocker {
|
|||
methodKey,
|
||||
)} property because it is not a function; ${this._typeOf(
|
||||
original,
|
||||
)} given instead`,
|
||||
)} given instead.${
|
||||
typeof original !== 'object'
|
||||
? ` If you are trying to mock a property, use \`jest.replaceProperty(object, '${String(
|
||||
methodKey,
|
||||
)}', value)\` instead.`
|
||||
: ''
|
||||
}`,
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -1208,7 +1257,13 @@ export class ModuleMocker {
|
|||
propertyKey,
|
||||
)} property because it is not a function; ${this._typeOf(
|
||||
original,
|
||||
)} given instead`,
|
||||
)} given instead.${
|
||||
typeof original !== 'object'
|
||||
? ` If you are trying to mock a property, use \`jest.replaceProperty(object, '${String(
|
||||
propertyKey,
|
||||
)}', value)\` instead.`
|
||||
: ''
|
||||
}`,
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -1230,6 +1285,117 @@ export class ModuleMocker {
|
|||
return descriptor[accessType] as Mock;
|
||||
}
|
||||
|
||||
replaceProperty<
|
||||
T extends object,
|
||||
K extends PropertyLikeKeys<T>,
|
||||
V extends T[K],
|
||||
>(object: T, propertyKey: K, value: V): Replaced<T[K]> {
|
||||
if (object === undefined || object == null) {
|
||||
throw new Error(
|
||||
`replaceProperty could not find an object on which to replace ${String(
|
||||
propertyKey,
|
||||
)}`,
|
||||
);
|
||||
}
|
||||
|
||||
if (propertyKey === undefined || propertyKey === null) {
|
||||
throw new Error('No property name supplied');
|
||||
}
|
||||
|
||||
if (typeof object !== 'object') {
|
||||
throw new Error(
|
||||
`Cannot mock property on a non-object value; ${this._typeOf(
|
||||
object,
|
||||
)} given`,
|
||||
);
|
||||
}
|
||||
|
||||
let descriptor = Object.getOwnPropertyDescriptor(object, propertyKey);
|
||||
let proto = Object.getPrototypeOf(object);
|
||||
while (!descriptor && proto !== null) {
|
||||
descriptor = Object.getOwnPropertyDescriptor(proto, propertyKey);
|
||||
proto = Object.getPrototypeOf(proto);
|
||||
}
|
||||
if (!descriptor) {
|
||||
throw new Error(`${String(propertyKey)} property does not exist`);
|
||||
}
|
||||
if (!descriptor.configurable) {
|
||||
throw new Error(`${String(propertyKey)} is not declared configurable`);
|
||||
}
|
||||
|
||||
if (descriptor.get !== undefined) {
|
||||
throw new Error(
|
||||
`Cannot mock the ${String(
|
||||
propertyKey,
|
||||
)} property because it has a getter. Use \`jest.spyOn(object, '${String(
|
||||
propertyKey,
|
||||
)}', 'get').mockReturnValue(value)\` instead.`,
|
||||
);
|
||||
}
|
||||
|
||||
if (descriptor.set !== undefined) {
|
||||
throw new Error(
|
||||
`Cannot mock the ${String(
|
||||
propertyKey,
|
||||
)} property because it has a setter. Use \`jest.spyOn(object, '${String(
|
||||
propertyKey,
|
||||
)}', 'set').mockReturnValue(value)\` instead.`,
|
||||
);
|
||||
}
|
||||
|
||||
if (typeof descriptor.value === 'function') {
|
||||
throw new Error(
|
||||
`Cannot mock the ${String(
|
||||
propertyKey,
|
||||
)} property because it is a function. Use \`jest.spyOn(object, '${String(
|
||||
propertyKey,
|
||||
)}')\` instead.`,
|
||||
);
|
||||
}
|
||||
|
||||
const existingRestore = this._findReplacedProperty(object, propertyKey);
|
||||
|
||||
if (existingRestore) {
|
||||
return existingRestore.replaced.replaceValue(value);
|
||||
}
|
||||
|
||||
const isPropertyOwner = Object.prototype.hasOwnProperty.call(
|
||||
object,
|
||||
propertyKey,
|
||||
);
|
||||
const originalValue = descriptor.value;
|
||||
|
||||
const restore: ReplacedPropertyRestorer<T, K> = () => {
|
||||
if (isPropertyOwner) {
|
||||
object[propertyKey] = originalValue;
|
||||
} else {
|
||||
delete object[propertyKey];
|
||||
}
|
||||
};
|
||||
|
||||
const replaced: Replaced<T[K]> = {
|
||||
replaceValue: value => {
|
||||
object[propertyKey] = value;
|
||||
|
||||
return replaced;
|
||||
},
|
||||
|
||||
restore: () => {
|
||||
restore();
|
||||
|
||||
this._spyState.delete(restore);
|
||||
},
|
||||
};
|
||||
|
||||
restore.object = object;
|
||||
restore.property = propertyKey;
|
||||
restore.replaced = replaced;
|
||||
|
||||
this._spyState.add(restore);
|
||||
|
||||
return replaced.replaceValue(value);
|
||||
}
|
||||
|
||||
clearAllMocks(): void {
|
||||
this._mockState = new WeakMap();
|
||||
}
|
||||
|
@ -1266,3 +1432,4 @@ const JestMock = new ModuleMocker(globalThis);
|
|||
export const fn = JestMock.fn.bind(JestMock);
|
||||
export const spyOn = JestMock.spyOn.bind(JestMock);
|
||||
export const mocked = JestMock.mocked.bind(JestMock);
|
||||
export const replaceProperty = JestMock.replaceProperty.bind(JestMock);
|
||||
|
|
|
@ -0,0 +1,37 @@
|
|||
/**
|
||||
* Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
let createRuntime;
|
||||
let obj;
|
||||
|
||||
describe('Runtime', () => {
|
||||
beforeEach(() => {
|
||||
createRuntime = require('createRuntime');
|
||||
|
||||
obj = {
|
||||
property: 1,
|
||||
};
|
||||
});
|
||||
|
||||
describe('jest.replaceProperty', () => {
|
||||
it('should work', async () => {
|
||||
const runtime = await createRuntime(__filename);
|
||||
const root = runtime.requireModule(runtime.__mockRootPath);
|
||||
const mocked = root.jest.replaceProperty(obj, 'property', 2);
|
||||
expect(obj.property).toBe(2);
|
||||
|
||||
mocked.replaceValue(3);
|
||||
expect(obj.property).toBe(3);
|
||||
|
||||
mocked.restore();
|
||||
expect(obj.property).toBe(1);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -2193,6 +2193,9 @@ export default class Runtime {
|
|||
'Your test environment does not support `mocked`, please update it.',
|
||||
);
|
||||
});
|
||||
const replaceProperty = this._moduleMocker.replaceProperty.bind(
|
||||
this._moduleMocker,
|
||||
);
|
||||
|
||||
const setTimeout = (timeout: number) => {
|
||||
this._environment.global[testTimeoutSymbol] = timeout;
|
||||
|
@ -2253,6 +2256,7 @@ export default class Runtime {
|
|||
mock,
|
||||
mocked,
|
||||
now: () => _getFakeTimers().now(),
|
||||
replaceProperty,
|
||||
requireActual: moduleName => this.requireActual(from, moduleName),
|
||||
requireMock: moduleName => this.requireMock(from, moduleName),
|
||||
resetAllMocks,
|
||||
|
|
|
@ -266,6 +266,8 @@ expectType<ModuleMocker['fn']>(jest.fn);
|
|||
|
||||
expectType<ModuleMocker['spyOn']>(jest.spyOn);
|
||||
|
||||
expectType<ModuleMocker['replaceProperty']>(jest.replaceProperty);
|
||||
|
||||
// Mock<T>
|
||||
|
||||
expectType<Mock<() => boolean>>({} as jest.Mock<() => boolean>);
|
||||
|
@ -447,6 +449,12 @@ expectError(
|
|||
|
||||
expectAssignable<typeof someObject>(mockObjectB);
|
||||
|
||||
// Replaced
|
||||
|
||||
expectAssignable<jest.Replaced<number>>(
|
||||
jest.replaceProperty(someObject, 'propertyA', 123),
|
||||
);
|
||||
|
||||
// Spied
|
||||
|
||||
expectAssignable<jest.Spied<typeof someObject.methodA>>(
|
||||
|
|
Loading…
Reference in New Issue