Skip to content

Commit

Permalink
adding ts-jest mock util functions in jest-mock (#12089)
Browse files Browse the repository at this point in the history
  • Loading branch information
k-rajat19 committed Nov 29, 2021
1 parent c739748 commit ee24dfc
Show file tree
Hide file tree
Showing 4 changed files with 134 additions and 1 deletion.
1 change: 1 addition & 0 deletions CHANGELOG.md
Expand Up @@ -4,6 +4,7 @@

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

### Fixes
Expand Down
44 changes: 44 additions & 0 deletions docs/JestObjectAPI.md
Expand Up @@ -578,6 +578,50 @@ 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(mocked(foo.name).mock.calls).toHaveLength(1);
});
```

## Mock Timers

### `jest.useFakeTimers(implementation?: 'modern' | 'legacy')`
Expand Down
9 changes: 8 additions & 1 deletion packages/jest-mock/src/__tests__/index.test.ts
Expand Up @@ -9,7 +9,7 @@
/* eslint-disable local/ban-types-eventually, local/prefer-rest-params-eventually */

import vm, {Context} from 'vm';
import {ModuleMocker, fn, spyOn} from '../';
import {ModuleMocker, fn, mocked, spyOn} from '../';

describe('moduleMocker', () => {
let moduleMocker: ModuleMocker;
Expand Down Expand Up @@ -1452,6 +1452,13 @@ describe('moduleMocker', () => {
});
});

describe('mocked', () => {
it('should return unmodified input', () => {
const subject = {};
expect(mocked(subject)).toBe(subject);
});
});

test('`fn` and `spyOn` do not throw', () => {
expect(() => {
fn();
Expand Down
81 changes: 81 additions & 0 deletions packages/jest-mock/src/index.ts
Expand Up @@ -32,6 +32,77 @@ export type MockFunctionMetadata<
length?: number;
};

export type MockableFunction = (...args: Array<any>) => any;
export type MethodKeysOf<T> = {
[K in keyof T]: T[K] extends MockableFunction ? K : never;
}[keyof T];
export type PropertyKeysOf<T> = {
[K in keyof T]: T[K] extends MockableFunction ? never : K;
}[keyof T];

export type ArgumentsOf<T> = T extends (...args: infer A) => any ? A : never;

export type ConstructorArgumentsOf<T> = T extends new (...args: infer A) => any
? A
: never;
export type MaybeMockedConstructor<T> = T extends new (
...args: Array<any>
) => infer R
? MockInstance<R, ConstructorArgumentsOf<T>>
: T;
export type MockedFunction<T extends MockableFunction> = MockWithArgs<T> & {
[K in keyof T]: T[K];
};
export type MockedFunctionDeep<T extends MockableFunction> = MockWithArgs<T> &
MockedObjectDeep<T>;
export type MockedObject<T> = MaybeMockedConstructor<T> & {
[K in MethodKeysOf<T>]: T[K] extends MockableFunction
? MockedFunction<T[K]>
: T[K];
} & {[K in PropertyKeysOf<T>]: T[K]};
export type MockedObjectDeep<T> = MaybeMockedConstructor<T> & {
[K in MethodKeysOf<T>]: T[K] extends MockableFunction
? MockedFunctionDeep<T[K]>
: T[K];
} & {[K in PropertyKeysOf<T>]: MaybeMockedDeep<T[K]>};

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

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

export type ArgsType<T> = T extends (...args: infer A) => any ? A : never;
export type Mocked<T> = {
[P in keyof T]: T[P] extends (...args: Array<any>) => any
? MockInstance<ReturnType<T[P]>, ArgsType<T[P]>>
: T[P] extends Constructable
? MockedClass<T[P]>
: T[P];
} & T;
export type MockedClass<T extends Constructable> = MockInstance<
InstanceType<T>,
T extends new (...args: infer P) => any ? P : never
> & {
prototype: T extends {prototype: any} ? Mocked<T['prototype']> : never;
} & T;

export interface Constructable {
new (...args: Array<any>): any;
}

export interface MockWithArgs<T extends MockableFunction>
extends MockInstance<ReturnType<T>, ArgumentsOf<T>> {
new (...args: ConstructorArgumentsOf<T>): T;
(...args: ArgumentsOf<T>): ReturnType<T>;
}

export interface Mock<T, Y extends Array<unknown> = Array<unknown>>
extends Function,
MockInstance<T, Y> {
Expand Down Expand Up @@ -1109,9 +1180,19 @@ export class ModuleMocker {
private _typeOf(value: any): string {
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;
}
}

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

export const fn = JestMock.fn.bind(JestMock);
export const spyOn = JestMock.spyOn.bind(JestMock);
export const mocked = JestMock.mocked.bind(JestMock);

0 comments on commit ee24dfc

Please sign in to comment.