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)!: make jest.mocked() helper to always return deeply mock-typed object #12516

Closed
wants to merge 3 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Expand Up @@ -28,6 +28,7 @@
- `[jest-mock]` Improve `isMockFunction` to infer types of passed function ([#12442](https://github.com/facebook/jest/pull/12442))
- `[jest-mock]` [**BREAKING**] Improve the usage of `jest.fn` generic type argument ([#12489](https://github.com/facebook/jest/pull/12489))
- `[jest-mock]` Add support for auto-mocking async generator functions ([#11080](https://github.com/facebook/jest/pull/11080))
- `[jest-mock]` [**BREAKING**] Make `jest.mocked()` helper to always return deeply mock-typed object. The `MaybeMockedDeep`, `MockedFunctionDeep` and `MockedObjectDeep` types are now exposed as `MaybeMocked`, `MockedFunction` and `MockedObject` ([#12516](https://github.com/facebook/jest/pull/12516))
- `[jest-resolve]` [**BREAKING**] Add support for `package.json` `exports` ([#11961](https://github.com/facebook/jest/pull/11961), [#12373](https://github.com/facebook/jest/pull/12373))
- `[jest-resolve, jest-runtime]` Add support for `data:` URI import and mock ([#12392](https://github.com/facebook/jest/pull/12392))
- `[jest-resolve, jest-runtime]` Add support for async resolver ([#11540](https://github.com/facebook/jest/pull/11540))
Expand Down
22 changes: 5 additions & 17 deletions docs/JestObjectAPI.md
Expand Up @@ -584,16 +584,13 @@ 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)`
### `jest.mocked<T>(mockedObject)`

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`).
The `mocked` test helper provides typings on your mocked modules and even their deep methods, based on the typing of its source.

_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
```ts title="foo.ts"
export const foo = {
a: {
b: {
Expand All @@ -602,30 +599,21 @@ export const foo = {
},
},
},
name: () => 'foo',
};
```

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

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

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

## Mock Timers
Expand Down
25 changes: 25 additions & 0 deletions packages/jest-mock/__typetests__/mocked.test.ts
@@ -0,0 +1,25 @@
/**
* 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.
*/

import {expectType} from 'tsd-lite';
import {mocked} from 'jest-mock';

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

const mockedFoo = mocked(foo);

expectType<string>(mockedFoo.a.b.c.hello('me'));

expectType<Array<[name: string]>>(mockedFoo.a.b.c.hello.mock.calls);
31 changes: 5 additions & 26 deletions packages/jest-mock/src/index.ts
Expand Up @@ -67,37 +67,21 @@ export interface MockWithArgs<T extends FunctionLike> extends MockInstance<T> {
(...args: Parameters<T>): ReturnType<T>;
}

export type MockedFunction<T extends FunctionLike> = MockWithArgs<T> & {
[K in keyof T]: T[K];
};

export type MockedFunctionDeep<T extends FunctionLike> = MockWithArgs<T> &
MockedObjectDeep<T>;
export type MockedFunction<T extends FunctionLike> = MockWithArgs<T> &
MockedObject<T>;

export type MockedObject<T> = MaybeMockedConstructor<T> & {
[K in MethodLikeKeys<T>]: T[K] extends FunctionLike
? MockedFunction<T[K]>
: T[K];
} & {[K in PropertyLikeKeys<T>]: T[K]};

export type MockedObjectDeep<T> = MaybeMockedConstructor<T> & {
[K in MethodLikeKeys<T>]: T[K] extends FunctionLike
? MockedFunctionDeep<T[K]>
: T[K];
} & {[K in PropertyLikeKeys<T>]: MaybeMockedDeep<T[K]>};
} & {[K in PropertyLikeKeys<T>]: MaybeMocked<T[K]>};

export type MaybeMocked<T> = T extends FunctionLike
? MockedFunction<T>
: T extends object
? MockedObject<T>
: T;

export type MaybeMockedDeep<T> = T extends FunctionLike
? MockedFunctionDeep<T>
: T extends object
? MockedObjectDeep<T>
: T;

export type Mocked<T> = {
[P in keyof T]: T[P] extends FunctionLike
? MockInstance<T[P]>
Expand Down Expand Up @@ -1216,13 +1200,8 @@ export class ModuleMocker {
return value == null ? `${value}` : typeof value;
}

// the typings test helper
mocked<T>(item: T, deep?: false): MaybeMocked<T>;

mocked<T>(item: T, deep: true): MaybeMockedDeep<T>;

mocked<T>(item: T, _deep = false): MaybeMocked<T> | MaybeMockedDeep<T> {
return item as any;
mocked<T>(mockedObject: T): MaybeMocked<T> {
return mockedObject as MaybeMocked<T>;
}
}

Expand Down