Skip to content

Commit

Permalink
refactor(jest-mock)!: change the default jest.mocked helper’s behav…
Browse files Browse the repository at this point in the history
…iour to deep mocked (#13125)
  • Loading branch information
mrazauskas authored Aug 13, 2022
1 parent 2f4340c commit 7d8c313
Show file tree
Hide file tree
Showing 8 changed files with 129 additions and 67 deletions.
2 changes: 1 addition & 1 deletion .eslintrc.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -180,7 +180,7 @@ module.exports = {
},
},
{
files: ['website/**/*'],
files: ['docs/**/*', 'website/**/*'],
rules: {
'import/order': 'off',
'import/sort-keys': 'off',
Expand Down
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
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
60 changes: 15 additions & 45 deletions docs/JestObjectAPI.md
Original file line number Diff line number Diff line change
Expand Up @@ -282,6 +282,20 @@ Modules that are mocked with `jest.mock` are mocked only for the file that calls

Returns the `jest` object for chaining.

:::tip

Writing tests in TypeScript? Use [`jest.Mocked`](MockFunctionAPI.md/#jestmockedsource) utility type or [`jest.mocked()`](MockFunctionAPI.md/#jestmockedsource-options) helper method to have your mocked modules typed.

:::

### `jest.Mocked<Source>`

See [TypeScript Usage](MockFunctionAPI.md/#jestmockedsource) chapter of Mock Functions page for documentation.

### `jest.mocked(source, options?)`

See [TypeScript Usage](MockFunctionAPI.md/#jestmockedsource-options) chapter of Mock Functions page for documentation.

### `jest.unmock(moduleName)`

Indicates that the module system should never return a mocked version of the specified module from `require()` (e.g. that it should always return the real module).
Expand Down Expand Up @@ -467,7 +481,7 @@ const returnsTrue = jest.fn(() => true);
console.log(returnsTrue()); // true;
```

:::note
:::tip

See [Mock Functions](MockFunctionAPI.md#jestfnimplementation) page for details on TypeScript usage.

Expand Down Expand Up @@ -598,50 +612,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)`

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
Original file line number Diff line number Diff line change
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 'node-fetch';

jest.mock('node-fetch');

let mockedFetch: jest.Mocked<typeof fetch>;

afterEach(() => {
mockedFetch.mockClear();
});

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(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.

Returns the `source` object.

```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
Original file line number Diff line number Diff line change
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 [`jest.mocked()`](MockFunctionAPI.md/#jestmockedsource-options) helper method now wraps types of deep members of passed object by default. If you have used the method with `true` as the second argument, 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}` as the second argument:

```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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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

0 comments on commit 7d8c313

Please sign in to comment.