Skip to content

Commit

Permalink
refactor(jest-mock)!: rework Mocked* utility types (#13123)
Browse files Browse the repository at this point in the history
  • Loading branch information
mrazauskas committed Aug 12, 2022
1 parent 1919ef1 commit f9718de
Show file tree
Hide file tree
Showing 7 changed files with 409 additions and 60 deletions.
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
? 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;
} & 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

0 comments on commit f9718de

Please sign in to comment.