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 @@ -5,6 +5,7 @@
- `[jest-config]` [**BREAKING**] Make `snapshotFormat` default to `escapeString: false` and `printBasicPrototype: false` ([#13036](https://github.com/facebook/jest/pull/13036))
- `[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-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
47 changes: 47 additions & 0 deletions docs/MockFunctionAPI.md
Expand Up @@ -520,3 +520,50 @@ test('calculate calls add', () => {
expect(mockAdd).toBeCalledWith(1, 2);
});
```

### `jest.mocked<T>(source: T, {shallow?: boolean})`

The `mocked()` type helper method wraps `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.

Example:

```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);

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()` method now wraps deep members of passed object by default. If you 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});
```
9 changes: 5 additions & 4 deletions packages/jest-environment/src/index.ts
Expand Up @@ -201,6 +201,11 @@ export interface Jest {
```
*/
requireActual: (moduleName: string) => unknown;
/**
* Wraps `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 +229,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
28 changes: 20 additions & 8 deletions packages/jest-types/__typetests__/jest.test.ts
Expand Up @@ -7,7 +7,13 @@

import {expectAssignable, expectError, expectType} from 'tsd-lite';
import {jest} from '@jest/globals';
import type {Mock, ModuleMocker, SpyInstance} from 'jest-mock';
import type {
Mock,
Mocked,
MockedShallow,
ModuleMocker,
SpyInstance,
} from 'jest-mock';

expectType<typeof jest>(
jest
Expand Down Expand Up @@ -210,7 +216,7 @@ expectType<ModuleMocker['fn']>(jest.fn);

expectType<ModuleMocker['spyOn']>(jest.spyOn);

// deep mocked()
// mocked()

class SomeClass {
constructor(one: string, two?: boolean) {}
Expand Down Expand Up @@ -248,9 +254,17 @@ const someObject = {
someClassInstance: new SomeClass('value'),
};

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'));

expectError(jest.mocked('abc', true));
const mockObjectA = jest.mocked(someObject);

expectType<[]>(mockObjectA.methodA.mock.calls[0]);
expectType<[b: string]>(mockObjectA.methodB.mock.calls[0]);
Expand Down Expand Up @@ -303,11 +317,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