Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor(jest-mock)!: change the default jest.mocked helper’s behaviour to deep mocked #13125

Merged
merged 14 commits into from Aug 13, 2022
1 change: 1 addition & 0 deletions CHANGELOG.md
Expand Up @@ -6,6 +6,7 @@
- `[jest-environment-jsdom]` [**BREAKING**] Upgrade to `jsdom@20` ([#13037](https://github.com/facebook/jest/pull/13037), [#13058](https://github.com/facebook/jest/pull/13058))
- `[@jest/globals]` Add `jest.Mocked`, `jest.MockedClass`, `jest.MockedFunction` and `jest.MockedObject` utility types ([#12727](https://github.com/facebook/jest/pull/12727))
- `[jest-mock]` [**BREAKING**] Refactor `Mocked*` utility types. `MaybeMockedDeep` and `MaybeMocked` became `Mocked` and `MockedShallow` respectively; only deep mocked variants of `MockedClass`, `MockedFunction` and `MockedObject` are exported ([#13123](https://github.com/facebook/jest/pull/13123), [#13124](https://github.com/facebook/jest/pull/13124))
- `[jest-mock]` [**BREAKING**] Change the default `jest.mocked` helper’s behavior to deep mocked ([#13125](https://github.com/facebook/jest/pull/13125))
- `[jest-worker]` Adds `workerIdleMemoryLimit` option which is used as a check for worker memory leaks >= Node 16.11.0 and recycles child workers as required. ([#13056](https://github.com/facebook/jest/pull/13056), [#13105](https://github.com/facebook/jest/pull/13105), [#13106](https://github.com/facebook/jest/pull/13106), [#13107](https://github.com/facebook/jest/pull/13107))
- `[pretty-format]` [**BREAKING**] Remove `ConvertAnsi` plugin in favour of `jest-serializer-ansi-escapes` ([#13040](https://github.com/facebook/jest/pull/13040))

Expand Down
44 changes: 0 additions & 44 deletions docs/JestObjectAPI.md
Expand Up @@ -598,50 +598,6 @@ Returns the `jest` object for chaining.

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.

### `jest.mocked<T>(item: T, deep = false)`
mrazauskas marked this conversation as resolved.
Show resolved Hide resolved
mrazauskas marked this conversation as resolved.
Show resolved Hide resolved

The `mocked` test helper provides typings on your mocked modules and even their deep methods, based on the typing of its source. It makes use of the latest TypeScript feature, so you even have argument types completion in the IDE (as opposed to `jest.MockInstance`).

_Note: while it needs to be a function so that input type is changed, the helper itself does nothing else than returning the given input value._

Example:

```ts
// foo.ts
export const foo = {
a: {
b: {
c: {
hello: (name: string) => `Hello, ${name}`,
},
},
},
name: () => 'foo',
};
```

```ts
// foo.spec.ts
import {foo} from './foo';
jest.mock('./foo');

// here the whole foo var is mocked deeply
const mockedFoo = jest.mocked(foo, true);

test('deep', () => {
// there will be no TS error here, and you'll have completion in modern IDEs
mockedFoo.a.b.c.hello('me');
// same here
expect(mockedFoo.a.b.c.hello.mock.calls).toHaveLength(1);
});

test('direct', () => {
foo.name();
// here only foo.name is mocked (or its methods if it's an object)
expect(jest.mocked(foo.name).mock.calls).toHaveLength(1);
});
```

## Fake Timers

### `jest.useFakeTimers(fakeTimersConfig?)`
Expand Down
53 changes: 52 additions & 1 deletion docs/MockFunctionAPI.md
Expand Up @@ -526,13 +526,17 @@ test('calculate calls add', () => {
The `jest.Mocked<Source>` utility type returns the `Source` type wrapped with type definitions of Jest mock function.

```ts
import fetch from 'node-fetch';
import {expect, jest, test} from '@jest/globals';
import type {fetch} from './fetch';
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this looks wrong - a type import will fail the typeof below. But why not keep the node-fetch import?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hm.. Checked it. type works, TS does not complain. Similar to the above: https://github.com/facebook/jest/blob/2f4340cb1da435a9b147e238f7034f99653b8070/docs/MockFunctionAPI.md?plain=1#L500-L505

I was not happy that ESLint (import/order) is pushing to keep 'fetch' import above '@jest/globals'. Probably some ignore comment could fix this.


jest.mock('node-fetch');

let mockedFetch: jest.Mocked<typeof fetch>;

afterEach(() => {
mockedFetch = resetMockedFetch();
});

test('makes correct call', () => {
mockedFetch = getMockedFetch();
// ...
Expand All @@ -545,3 +549,50 @@ 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.mocked<T>(source: T, {shallow?: boolean})`

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}` option to disable the deeply mocked behavior.

Returns the input value.

```ts title="song.ts"
export const song = {
one: {
more: {
time: (t: number) => {
return t;
},
},
},
};
```

```ts title="song.test.ts"
import {expect, jest, test} from '@jest/globals';
import {song} from './song';

jest.mock('./song');
jest.spyOn(console, 'log');

const mockedSong = jest.mocked(song);
// or through `jest.Mocked<Source>`
// const mockedSong = song as jest.Mocked<typeof song>;

test('deep method is typed correctly', () => {
mockedSong.one.more.time.mockReturnValue(12);

expect(mockedSong.one.more.time(10)).toBe(12);
expect(mockedSong.one.more.time.mock.calls).toHaveLength(1);
});

test('direct usage', () => {
jest.mocked(console.log).mockImplementation(() => {
return;
});

console.log('one more time');

expect(jest.mocked(console.log).mock.calls).toHaveLength(1);
});
```
36 changes: 32 additions & 4 deletions docs/UpgradingToJest29.md
Expand Up @@ -40,10 +40,38 @@ If you want to keep the old behavior, you can set the `snapshotFormat` property

Notably, `jsdom@20` includes support for `crypto.getRandomValues()`, which means packages like `jsdom` and `nanoid`, which doesn't work properly in Jest@28, can work without extra polyfills.

## `jest-mock`
## `pretty-format`

Exports of `Mocked*` utility types changed. `MaybeMockedDeep` and `MaybeMocked` now are exported as `Mocked` and `MockedShallow` respectively; only deep mocked variants of `MockedClass`, `MockedFunction` and `MockedObject` are exposed.
`ConvertAnsi` plugin is removed from `pretty-format` package in favour of [`jest-serializer-ansi-escapes`](https://github.com/mrazauskas/jest-serializer-ansi-escapes).

## `pretty-format`
### `jest-mock`

Exports of `Mocked*` utility types from `jest-mock` package have changed. `MaybeMockedDeep` and `MaybeMocked` now are exported as `Mocked` and `MockedShallow` respectively; only deep mocked variants of `MockedClass`, `MockedFunction` and `MockedObject` are exposed.

## TypeScript

:::info

The TypeScript examples from this page will only work as documented if you import `jest` from `'@jest/globals'`:

`ConvertAnsi` plugin is removed in favour of [`jest-serializer-ansi-escapes`](https://github.com/mrazauskas/jest-serializer-ansi-escapes).
```ts
import {jest} from '@jest/globals';
```

:::

### `jest.mocked()`

The `mocked()` helper method now wraps types of deep members of passed object by default. If you have used the method with `true` argument before, remove it to avoid type errors:

```diff
- const mockedObject = jest.mocked(someObject, true);
+ const mockedObject = jest.mocked(someObject);
```

To have the old shallow mocked behavior, pass `{shallow: true}` option:

```diff
- const mockedObject = jest.mocked(someObject);
+ const mockedObject = jest.mocked(someObject, {shallow: true});
```
10 changes: 6 additions & 4 deletions packages/jest-environment/src/index.ts
Expand Up @@ -201,6 +201,12 @@ export interface Jest {
```
*/
requireActual: (moduleName: string) => unknown;
/**
* Wraps types of the `source` object and its deep members with type definitions
* of Jest mock function. Pass `{shallow: true}` option to disable the deeply
* mocked behavior.
*/
mocked: ModuleMocker['mocked'];
/**
* Returns a mock module instead of the actual module, bypassing all checks
* on whether the module should be required normally or not.
Expand All @@ -224,10 +230,6 @@ export interface Jest {
* with `jest.spyOn()`; other mocks will require you to manually restore them.
*/
restoreAllMocks(): Jest;
/**
* Wraps an object or a module with mock type definitions.
*/
mocked: ModuleMocker['mocked'];
/**
* Runs failed tests n-times until they pass or until the max number of
* retries is exhausted.
Expand Down
13 changes: 8 additions & 5 deletions packages/jest-mock/src/index.ts
Expand Up @@ -1216,13 +1216,16 @@ export class ModuleMocker {
return value == null ? `${value}` : typeof value;
}

mocked<T extends object>(item: T, deep?: false): MockedShallow<T>;
mocked<T extends object>(item: T, deep: true): Mocked<T>;
mocked<T extends object>(source: T, options?: {shallow: false}): Mocked<T>;
mocked<T extends object>(
item: T,
_deep = false,
source: T,
options: {shallow: true},
): MockedShallow<T>;
mocked<T extends object>(
source: T,
_options?: {shallow: boolean},
): Mocked<T> | MockedShallow<T> {
return item as Mocked<T> | MockedShallow<T>;
return source as Mocked<T> | MockedShallow<T>;
}
}

Expand Down
21 changes: 14 additions & 7 deletions packages/jest-types/__typetests__/jest.test.ts
Expand Up @@ -13,6 +13,7 @@ import type {
MockedClass,
MockedFunction,
MockedObject,
MockedShallow,
ModuleMocker,
SpyInstance,
} from 'jest-mock';
Expand Down Expand Up @@ -276,11 +277,19 @@ expectType<MockedObject<typeof someObject>>(
someObject as jest.MockedObject<typeof someObject>,
);

// deep mocked()
// mocked()

const mockObjectA = jest.mocked(someObject, true);
expectType<Mocked<typeof someObject>>(jest.mocked(someObject));
expectType<Mocked<typeof someObject>>(
jest.mocked(someObject, {shallow: false}),
);
expectType<MockedShallow<typeof someObject>>(
jest.mocked(someObject, {shallow: true}),
);

expectError(jest.mocked('abc', true));
expectError(jest.mocked('abc'));

const mockObjectA = jest.mocked(someObject);

expectType<[]>(mockObjectA.methodA.mock.calls[0]);
expectType<[b: string]>(mockObjectA.methodB.mock.calls[0]);
Expand Down Expand Up @@ -333,11 +342,9 @@ expectError(

expectAssignable<typeof someObject>(mockObjectA);

// mocked()

const mockObjectB = jest.mocked(someObject);
// shallow mocked()

expectError(jest.mocked('abc'));
const mockObjectB = jest.mocked(someObject, {shallow: true});

expectType<[]>(mockObjectB.methodA.mock.calls[0]);
expectType<[b: string]>(mockObjectB.methodB.mock.calls[0]);
Expand Down