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)!: rework Mocked* utility types #13123

Merged
merged 6 commits into from Aug 12, 2022
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-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))
- `[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
4 changes: 4 additions & 0 deletions docs/UpgradingToJest29.md
Expand Up @@ -40,6 +40,10 @@ 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`

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.

## `pretty-format`

`ConvertAnsi` plugin is removed in favour of [`jest-serializer-ansi-escapes`](https://github.com/mrazauskas/jest-serializer-ansi-escapes).
6 changes: 5 additions & 1 deletion examples/typescript/__tests__/calc.test.ts
@@ -1,3 +1,7 @@
// Copyright (c) 2014-present, Facebook, Inc. All rights reserved.

import {jest} from '@jest/globals';

import Memory from '../memory';
import sub from '../sub';
import sum from '../sum';
Expand All @@ -9,7 +13,7 @@ jest.mock('../sum');

const mockSub = jest.mocked(sub);
const mockSum = jest.mocked(sum);
const MockMemory = Memory as jest.MockedClass<typeof Memory>;
const MockMemory = jest.mocked(Memory);

describe('calc - mocks', () => {
const memory = new MockMemory();
Expand Down
218 changes: 218 additions & 0 deletions packages/jest-mock/__typetests__/Mocked.test.ts
@@ -0,0 +1,218 @@
/**
* 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 {expectAssignable, expectError, expectType} from 'tsd-lite';
import type {MockInstance, Mocked} from 'jest-mock';

/// mocks class

class SomeClass {
constructor(one: string, two?: boolean) {}

methodA() {
return true;
}
methodB(a: string, b?: number) {
return;
}
}

const MockSomeClass = SomeClass as Mocked<typeof SomeClass>;

expectType<[one: string, two?: boolean]>(MockSomeClass.mock.calls[0]);

expectType<[]>(MockSomeClass.prototype.methodA.mock.calls[0]);
expectType<[a: string, b?: number]>(
MockSomeClass.prototype.methodB.mock.calls[0],
);

expectError(MockSomeClass.prototype.methodA.mockReturnValue('true'));
expectError(
MockSomeClass.prototype.methodB.mockImplementation(
(a: string, b?: string) => {
return;
},
),
);

expectType<[]>(MockSomeClass.mock.instances[0].methodA.mock.calls[0]);
expectType<[a: string, b?: number]>(
MockSomeClass.prototype.methodB.mock.calls[0],
);

const mockSomeInstance = new MockSomeClass('a') as Mocked<
InstanceType<typeof MockSomeClass>
>;

expectType<[]>(mockSomeInstance.methodA.mock.calls[0]);
expectType<[a: string, b?: number]>(mockSomeInstance.methodB.mock.calls[0]);

expectError(mockSomeInstance.methodA.mockReturnValue('true'));
expectError(
mockSomeInstance.methodB.mockImplementation((a: string, b?: string) => {
return;
}),
);

expectAssignable<SomeClass>(mockSomeInstance);

// mocks function

function someFunction(a: string, b?: number): boolean {
return true;
}

const mockFunction = someFunction as Mocked<typeof someFunction>;

expectType<[a: string, b?: number]>(mockFunction.mock.calls[0]);

expectError(mockFunction.mockReturnValue(123));
expectError(mockFunction.mockImplementation((a: boolean, b?: number) => true));

expectAssignable<typeof someFunction>(mockFunction);

// mocks async function

async function someAsyncFunction(a: Array<boolean>): Promise<string> {
return 'true';
}

const mockAsyncFunction = someAsyncFunction as Mocked<typeof someAsyncFunction>;

expectType<[Array<boolean>]>(mockAsyncFunction.mock.calls[0]);

expectError(mockAsyncFunction.mockResolvedValue(123));
expectError(
mockAsyncFunction.mockImplementation((a: Array<boolean>) =>
Promise.resolve(true),
),
);

expectAssignable<typeof someAsyncFunction>(mockAsyncFunction);

// mocks function object

interface SomeFunctionObject {
(a: number, b?: string): void;
one: {
(oneA: number, oneB?: boolean): boolean;
more: {
time: {
(time: number): void;
};
};
};
}

declare const someFunctionObject: SomeFunctionObject;

const mockFunctionObject = someFunctionObject as Mocked<
typeof someFunctionObject
>;

expectType<[a: number, b?: string]>(mockFunctionObject.mock.calls[0]);

expectError(mockFunctionObject.mockReturnValue(123));
expectError(mockFunctionObject.mockImplementation(() => true));

expectType<[time: number]>(mockFunctionObject.one.more.time.mock.calls[0]);

expectError(mockFunctionObject.one.more.time.mockReturnValue(123));
expectError(
mockFunctionObject.one.more.time.mockImplementation((time: string) => {
return;
}),
);

expectAssignable<typeof someFunctionObject>(mockFunctionObject);

// mocks object

const someObject = {
SomeClass,

methodA() {
return;
},
methodB(b: string) {
return true;
},
methodC: (c: number) => true,

one: {
more: {
time: (t: number) => {
return;
},
},
},

propertyA: 123,
propertyB: 'value',

someClassInstance: new SomeClass('value'),
};

const mockObject = someObject as Mocked<typeof someObject>;

expectType<[]>(mockObject.methodA.mock.calls[0]);
expectType<[b: string]>(mockObject.methodB.mock.calls[0]);
expectType<[c: number]>(mockObject.methodC.mock.calls[0]);

expectType<[t: number]>(mockObject.one.more.time.mock.calls[0]);

expectType<[one: string, two?: boolean]>(mockObject.SomeClass.mock.calls[0]);
expectType<[]>(mockObject.SomeClass.prototype.methodA.mock.calls[0]);
expectType<[a: string, b?: number]>(
mockObject.SomeClass.prototype.methodB.mock.calls[0],
);

expectType<[]>(mockObject.someClassInstance.methodA.mock.calls[0]);
expectType<[a: string, b?: number]>(
mockObject.someClassInstance.methodB.mock.calls[0],
);

expectError(mockObject.methodA.mockReturnValue(123));
expectError(mockObject.methodA.mockImplementation((a: number) => 123));
expectError(mockObject.methodB.mockReturnValue(123));
expectError(mockObject.methodB.mockImplementation((b: number) => 123));
expectError(mockObject.methodC.mockReturnValue(123));
expectError(mockObject.methodC.mockImplementation((c: number) => 123));

expectError(mockObject.one.more.time.mockReturnValue(123));
expectError(mockObject.one.more.time.mockImplementation((t: boolean) => 123));

expectError(mockObject.SomeClass.prototype.methodA.mockReturnValue(123));
expectError(
mockObject.SomeClass.prototype.methodA.mockImplementation((a: number) => 123),
);
expectError(mockObject.SomeClass.prototype.methodB.mockReturnValue(123));
expectError(
mockObject.SomeClass.prototype.methodB.mockImplementation((a: number) => 123),
);

expectError(mockObject.someClassInstance.methodA.mockReturnValue(123));
expectError(
mockObject.someClassInstance.methodA.mockImplementation((a: number) => 123),
);
expectError(mockObject.someClassInstance.methodB.mockReturnValue(123));
expectError(
mockObject.someClassInstance.methodB.mockImplementation((a: number) => 123),
);

expectAssignable<typeof someObject>(mockObject);

// mocks 'console' object

const mockConsole = console as Mocked<typeof console>;

expectAssignable<typeof console.log>(
mockConsole.log.mockImplementation(() => {}),
);
expectAssignable<MockInstance<typeof console.log>>(
mockConsole.log.mockImplementation(() => {}),
);
2 changes: 1 addition & 1 deletion packages/jest-mock/__typetests__/tsconfig.json
Expand Up @@ -6,7 +6,7 @@
"noUnusedParameters": false,
"skipLibCheck": true,

"types": []
"types": ["node"]
},
"include": ["./**/*"]
}
88 changes: 35 additions & 53 deletions packages/jest-mock/src/index.ts
Expand Up @@ -47,68 +47,50 @@ export type PropertyLikeKeys<T> = Exclude<
ConstructorLikeKeys<T> | MethodLikeKeys<T>
>;

// TODO Figure out how to replace this with TS ConstructorParameters utility type
// https://www.typescriptlang.org/docs/handbook/utility-types.html#constructorparameterstype
type ConstructorParameters<T> = T extends new (...args: infer P) => any
Comment on lines -50 to -52
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done ;D

? P
: never;

export type MaybeMockedConstructor<T> = T extends new (
...args: Array<any>
) => infer R
? MockInstance<(...args: ConstructorParameters<T>) => R>
: T;

export interface MockWithArgs<T extends FunctionLike> extends MockInstance<T> {
new (...args: ConstructorParameters<T>): T;
(...args: Parameters<T>): ReturnType<T>;
}
export type MockedClass<T extends ClassLike> = MockInstance<
(...args: ConstructorParameters<T>) => Mocked<InstanceType<T>>
> &
MockedObject<T>;

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

export type MockedFunctionDeep<T extends FunctionLike> = MockWithArgs<T> &
MockedObjectDeep<T>;
type MockedFunctionShallow<T extends FunctionLike> = MockInstance<T> & T;

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

export type MockedObjectDeep<T> = MaybeMockedConstructor<T> & {
[K in MethodLikeKeys<T>]: T[K] extends FunctionLike
? MockedFunctionDeep<T[K]>
type MockedObjectShallow<T extends object> = {
[K in keyof T]: T[K] extends ClassLike
? MockedClass<T[K]>
: T[K] extends FunctionLike
? MockedFunctionShallow<T[K]>
: T[K];
} & {[K in PropertyLikeKeys<T>]: MaybeMockedDeep<T[K]>};
} & T;

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

export type MaybeMockedDeep<T> = T extends FunctionLike
? MockedFunctionDeep<T>
type MockedShallow<T extends object> = T extends ClassLike
? MockedClass<T>
: T extends FunctionLike
? MockedFunctionShallow<T>
: T extends object
? MockedObjectDeep<T>
? MockedObjectShallow<T>
: T;

export type Mocked<T> = {
[P in keyof T]: T[P] extends FunctionLike
? MockInstance<T[P]>
: T[P] extends ClassLike
? MockedClass<T[P]>
: T[P];
} & T;

export type MockedClass<T extends ClassLike> = MockInstance<
(args: T extends new (...args: infer P) => any ? P : never) => InstanceType<T>
> & {
prototype: T extends {prototype: any} ? Mocked<T['prototype']> : never;
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Might be this could be a solution for MockedClassShallow. Although I am in doubt if it is worse to make types more complicated than proposed in this PR.

} & T;

export type UnknownFunction = (...args: Array<unknown>) => unknown;

/**
Expand Down Expand Up @@ -1234,13 +1216,13 @@ 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 extends object>(item: T, deep?: false): MockedShallow<T>;
mocked<T extends object>(item: T, deep: true): Mocked<T>;
mocked<T extends object>(
item: T,
_deep = false,
): Mocked<T> | MockedShallow<T> {
return item as Mocked<T> | MockedShallow<T>;
}
}

Expand Down