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

adding ts-jest mock util functions in jest-mock #12089

Merged
merged 9 commits into from Nov 29, 2021
Merged
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 @@ -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);