Skip to content

Commit ee24dfc

Browse files
authored
adding ts-jest mock util functions in jest-mock (#12089)
1 parent c739748 commit ee24dfc

File tree

4 files changed

+134
-1
lines changed

4 files changed

+134
-1
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
- `[jest-core]` Add support for `testResultsProcessor` written in ESM ([#12006](https://github.com/facebook/jest/pull/12006))
66
- `[jest-diff, pretty-format]` Add `compareKeys` option for custom sorting of object keys ([#11992](https://github.com/facebook/jest/pull/11992))
7+
- `[jest-mock]` Add `ts-jest` mock util functions ([#12089](https://github.com/facebook/jest/pull/12089))
78
- `[expect]` Enhancing the `toHaveProperty` matcher to support array selection ([#12092](https://github.com/facebook/jest/pull/12092))
89

910
### Fixes

docs/JestObjectAPI.md

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -578,6 +578,50 @@ Returns the `jest` object for chaining.
578578

579579
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.
580580

581+
### `jest.mocked<T>(item: T, deep = false)`
582+
583+
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`).
584+
585+
_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._
586+
587+
Example:
588+
589+
```ts
590+
// foo.ts
591+
export const foo = {
592+
a: {
593+
b: {
594+
c: {
595+
hello: (name: string) => `Hello, ${name}`,
596+
},
597+
},
598+
},
599+
name: () => 'foo',
600+
};
601+
```
602+
603+
```ts
604+
// foo.spec.ts
605+
import {foo} from './foo';
606+
jest.mock('./foo');
607+
608+
// here the whole foo var is mocked deeply
609+
const mockedFoo = jest.mocked(foo, true);
610+
611+
test('deep', () => {
612+
// there will be no TS error here, and you'll have completion in modern IDEs
613+
mockedFoo.a.b.c.hello('me');
614+
// same here
615+
expect(mockedFoo.a.b.c.hello.mock.calls).toHaveLength(1);
616+
});
617+
618+
test('direct', () => {
619+
foo.name();
620+
// here only foo.name is mocked (or its methods if it's an object)
621+
expect(mocked(foo.name).mock.calls).toHaveLength(1);
622+
});
623+
```
624+
581625
## Mock Timers
582626

583627
### `jest.useFakeTimers(implementation?: 'modern' | 'legacy')`

packages/jest-mock/src/__tests__/index.test.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
/* eslint-disable local/ban-types-eventually, local/prefer-rest-params-eventually */
1010

1111
import vm, {Context} from 'vm';
12-
import {ModuleMocker, fn, spyOn} from '../';
12+
import {ModuleMocker, fn, mocked, spyOn} from '../';
1313

1414
describe('moduleMocker', () => {
1515
let moduleMocker: ModuleMocker;
@@ -1452,6 +1452,13 @@ describe('moduleMocker', () => {
14521452
});
14531453
});
14541454

1455+
describe('mocked', () => {
1456+
it('should return unmodified input', () => {
1457+
const subject = {};
1458+
expect(mocked(subject)).toBe(subject);
1459+
});
1460+
});
1461+
14551462
test('`fn` and `spyOn` do not throw', () => {
14561463
expect(() => {
14571464
fn();

packages/jest-mock/src/index.ts

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,77 @@ export type MockFunctionMetadata<
3232
length?: number;
3333
};
3434

35+
export type MockableFunction = (...args: Array<any>) => any;
36+
export type MethodKeysOf<T> = {
37+
[K in keyof T]: T[K] extends MockableFunction ? K : never;
38+
}[keyof T];
39+
export type PropertyKeysOf<T> = {
40+
[K in keyof T]: T[K] extends MockableFunction ? never : K;
41+
}[keyof T];
42+
43+
export type ArgumentsOf<T> = T extends (...args: infer A) => any ? A : never;
44+
45+
export type ConstructorArgumentsOf<T> = T extends new (...args: infer A) => any
46+
? A
47+
: never;
48+
export type MaybeMockedConstructor<T> = T extends new (
49+
...args: Array<any>
50+
) => infer R
51+
? MockInstance<R, ConstructorArgumentsOf<T>>
52+
: T;
53+
export type MockedFunction<T extends MockableFunction> = MockWithArgs<T> & {
54+
[K in keyof T]: T[K];
55+
};
56+
export type MockedFunctionDeep<T extends MockableFunction> = MockWithArgs<T> &
57+
MockedObjectDeep<T>;
58+
export type MockedObject<T> = MaybeMockedConstructor<T> & {
59+
[K in MethodKeysOf<T>]: T[K] extends MockableFunction
60+
? MockedFunction<T[K]>
61+
: T[K];
62+
} & {[K in PropertyKeysOf<T>]: T[K]};
63+
export type MockedObjectDeep<T> = MaybeMockedConstructor<T> & {
64+
[K in MethodKeysOf<T>]: T[K] extends MockableFunction
65+
? MockedFunctionDeep<T[K]>
66+
: T[K];
67+
} & {[K in PropertyKeysOf<T>]: MaybeMockedDeep<T[K]>};
68+
69+
export type MaybeMockedDeep<T> = T extends MockableFunction
70+
? MockedFunctionDeep<T>
71+
: T extends object
72+
? MockedObjectDeep<T>
73+
: T;
74+
75+
export type MaybeMocked<T> = T extends MockableFunction
76+
? MockedFunction<T>
77+
: T extends object
78+
? MockedObject<T>
79+
: T;
80+
81+
export type ArgsType<T> = T extends (...args: infer A) => any ? A : never;
82+
export type Mocked<T> = {
83+
[P in keyof T]: T[P] extends (...args: Array<any>) => any
84+
? MockInstance<ReturnType<T[P]>, ArgsType<T[P]>>
85+
: T[P] extends Constructable
86+
? MockedClass<T[P]>
87+
: T[P];
88+
} & T;
89+
export type MockedClass<T extends Constructable> = MockInstance<
90+
InstanceType<T>,
91+
T extends new (...args: infer P) => any ? P : never
92+
> & {
93+
prototype: T extends {prototype: any} ? Mocked<T['prototype']> : never;
94+
} & T;
95+
96+
export interface Constructable {
97+
new (...args: Array<any>): any;
98+
}
99+
100+
export interface MockWithArgs<T extends MockableFunction>
101+
extends MockInstance<ReturnType<T>, ArgumentsOf<T>> {
102+
new (...args: ConstructorArgumentsOf<T>): T;
103+
(...args: ArgumentsOf<T>): ReturnType<T>;
104+
}
105+
35106
export interface Mock<T, Y extends Array<unknown> = Array<unknown>>
36107
extends Function,
37108
MockInstance<T, Y> {
@@ -1109,9 +1180,19 @@ export class ModuleMocker {
11091180
private _typeOf(value: any): string {
11101181
return value == null ? '' + value : typeof value;
11111182
}
1183+
1184+
// the typings test helper
1185+
mocked<T>(item: T, deep?: false): MaybeMocked<T>;
1186+
1187+
mocked<T>(item: T, deep: true): MaybeMockedDeep<T>;
1188+
1189+
mocked<T>(item: T, _deep = false): MaybeMocked<T> | MaybeMockedDeep<T> {
1190+
return item as any;
1191+
}
11121192
}
11131193

11141194
const JestMock = new ModuleMocker(global as unknown as typeof globalThis);
11151195

11161196
export const fn = JestMock.fn.bind(JestMock);
11171197
export const spyOn = JestMock.spyOn.bind(JestMock);
1198+
export const mocked = JestMock.mocked.bind(JestMock);

0 commit comments

Comments
 (0)