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

feat(@jest/environment, jest-runtime): allow passing a generic type argument to jest.createMockFromModule<T>() method #13202

Merged
merged 17 commits into from Sep 3, 2022
Merged
52 changes: 39 additions & 13 deletions docs/JestObjectAPI.md
Expand Up @@ -5,6 +5,16 @@ title: The Jest Object

The `jest` object is automatically in scope within every test file. The methods in the `jest` object help create mocks and let you control Jest's overall behavior. It can also be imported explicitly by via `import {jest} from '@jest/globals'`.

:::info

The TypeScript examples from this page will only work as documented if you import global APIs from `'@jest/globals'`:

```ts
import {expect, jest, test} from '@jest/globals';
```

:::

## Methods

import TOCInline from '@theme/TOCInline';
Expand Down Expand Up @@ -96,18 +106,12 @@ _Note: this method was previously called `autoMockOn`. When using `babel-jest`,

### `jest.createMockFromModule(moduleName)`

##### renamed in Jest **26.0.0+**

Also under the alias: `.genMockFromModule(moduleName)`
mrazauskas marked this conversation as resolved.
Show resolved Hide resolved

Given the name of a module, use the automatic mocking system to generate a mocked version of the module for you.

This is useful when you want to create a [manual mock](ManualMocks.md) that extends the automatic mock's behavior.
This is useful when you want to create a [manual mock](ManualMocks.md) that extends the automatic mock's behavior:

Example:

```js title="utils.js"
export default {
```js tab={"span":2} title="utils.js"
module.exports = {
authorize: () => {
return 'token';
},
Expand All @@ -116,12 +120,34 @@ export default {
```

```js title="__tests__/createMockFromModule.test.js"
const utils = jest.createMockFromModule('../utils').default;
const utils = jest.createMockFromModule('../utils');

utils.isAuthorized = jest.fn(secret => secret === 'not wizard');

test('implementation created by jest.createMockFromModule', () => {
expect(utils.authorize.mock).toBeTruthy();
expect(utils.isAuthorized('not wizard')).toEqual(true);
expect(jest.isMockFunction(utils.authorize)).toBe(true);
expect(utils.isAuthorized('not wizard')).toBe(true);
});
```

```ts tab={"span":2} title="utils.ts"
export const utils = {
authorize: () => {
return 'token';
},
isAuthorized: (secret: string) => secret === 'wizard',
};
```

```ts title="__tests__/createMockFromModule.test.ts"
const {utils} =
jest.createMockFromModule<typeof import('../utils')>('../utils');

utils.isAuthorized = jest.fn((secret: string) => secret === 'not wizard');

test('implementation created by jest.createMockFromModule', () => {
expect(jest.isMockFunction(utils.authorize)).toBe(true);
expect(utils.isAuthorized('not wizard')).toBe(true);
});
```

Expand Down Expand Up @@ -180,7 +206,7 @@ module.exports = {
```

```js title="__tests__/example.test.js"
const example = jest.createMockFromModule('./example');
const example = jest.createMockFromModule('../example');

test('should run example code', () => {
// creates a new mocked function with no formal arguments.
Expand Down
7 changes: 4 additions & 3 deletions packages/jest-environment/src/index.ts
Expand Up @@ -8,7 +8,7 @@
import type {Context} from 'vm';
import type {LegacyFakeTimers, ModernFakeTimers} from '@jest/fake-timers';
import type {Circus, Config, Global} from '@jest/types';
import type {ModuleMocker} from 'jest-mock';
import type {Mocked, ModuleMocker} from 'jest-mock';

export type EnvironmentContext = {
console: Console;
Expand Down Expand Up @@ -92,7 +92,7 @@ export interface Jest {
* This is useful when you want to create a manual mock that extends the
* automatic mock's behavior.
*/
createMockFromModule(moduleName: string): unknown;
createMockFromModule<T = unknown>(moduleName: string): Mocked<T>;
/**
* Indicates that the module system should never return a mocked version of
* the specified module and its dependencies.
Expand Down Expand Up @@ -129,6 +129,7 @@ export interface Jest {
* Creates a mock function. Optionally takes a mock implementation.
*/
fn: ModuleMocker['fn'];
// TODO remove `genMockFromModule()` in Jest 30
/**
* Given the name of a module, use the automatic mocking system to generate a
* mocked version of the module for you.
Expand All @@ -138,7 +139,7 @@ export interface Jest {
*
* @deprecated Use `jest.createMockFromModule()` instead
*/
genMockFromModule(moduleName: string): unknown;
genMockFromModule<T = unknown>(moduleName: string): Mocked<T>;
/**
* When mocking time, `Date.now()` will also be mocked. If you for some reason
* need access to the real current time, you can invoke this function.
Expand Down
108 changes: 49 additions & 59 deletions packages/jest-mock/src/index.ts
Expand Up @@ -7,7 +7,7 @@

/* eslint-disable local/ban-types-eventually, local/prefer-rest-params-eventually */

export type MockFunctionMetadataType =
export type MockMetadataType =
| 'object'
| 'array'
| 'regexp'
Expand All @@ -17,20 +17,26 @@ export type MockFunctionMetadataType =
| 'null'
| 'undefined';

export type MockFunctionMetadata<
T extends UnknownFunction = UnknownFunction,
MetadataType = MockFunctionMetadataType,
> = {
// TODO remove re-export in Jest 30
export type MockFunctionMetadataType = MockMetadataType;

export type MockMetadata<T, MetadataType = MockMetadataType> = {
ref?: number;
members?: Record<string, MockFunctionMetadata<T>>;
members?: Record<string, MockMetadata<T>>;
mockImpl?: T;
name?: string;
refID?: number;
type?: MetadataType;
value?: ReturnType<T>;
value?: T;
length?: number;
};

// TODO remove re-export in Jest 30
export type MockFunctionMetadata<
T = unknown,
MetadataType = MockMetadataType,
> = MockMetadata<T, MetadataType>;

export type ClassLike = {new (...args: any): any};
export type FunctionLike = (...args: any) => any;

Expand Down Expand Up @@ -75,15 +81,15 @@ type MockedObjectShallow<T extends object> = {
: T[K];
} & T;

export type Mocked<T extends object> = T extends ClassLike
export type Mocked<T> = T extends ClassLike
mrazauskas marked this conversation as resolved.
Show resolved Hide resolved
? MockedClass<T>
: T extends FunctionLike
? MockedFunction<T>
: T extends object
? MockedObject<T>
: T;

export type MockedShallow<T extends object> = T extends ClassLike
export type MockedShallow<T> = T extends ClassLike
? MockedClass<T>
: T extends FunctionLike
? MockedFunctionShallow<T>
Expand Down Expand Up @@ -386,7 +392,7 @@ function getObjectType(value: unknown): string {
return Object.prototype.toString.apply(value).slice(8, -1);
}

function getType(ref?: unknown): MockFunctionMetadataType | null {
function getType(ref?: unknown): MockMetadataType | null {
const typeName = getObjectType(ref);
if (
typeName === 'Function' ||
Expand Down Expand Up @@ -560,39 +566,30 @@ export class ModuleMocker {
};
}

private _makeComponent<T extends UnknownFunction>(
metadata: MockFunctionMetadata<T, 'object'>,
private _makeComponent<T extends Record<string, any>>(
metadata: MockMetadata<T, 'object'>,
restore?: () => void,
): Record<string, any>;
private _makeComponent<T extends UnknownFunction>(
metadata: MockFunctionMetadata<T, 'array'>,
): T;
private _makeComponent<T extends Array<unknown>>(
metadata: MockMetadata<T, 'array'>,
restore?: () => void,
): Array<unknown>;
private _makeComponent<T extends UnknownFunction>(
metadata: MockFunctionMetadata<T, 'regexp'>,
): T;
private _makeComponent<T extends RegExp>(
metadata: MockMetadata<T, 'regexp'>,
restore?: () => void,
): RegExp;
private _makeComponent<T extends UnknownFunction>(
mrazauskas marked this conversation as resolved.
Show resolved Hide resolved
metadata: MockFunctionMetadata<
T,
'constant' | 'collection' | 'null' | 'undefined'
>,
): T;
private _makeComponent<T>(
metadata: MockMetadata<T, 'constant' | 'collection' | 'null' | 'undefined'>,
restore?: () => void,
): T;
private _makeComponent<T extends UnknownFunction>(
metadata: MockFunctionMetadata<T, 'function'>,
metadata: MockMetadata<T, 'function'>,
restore?: () => void,
): Mock<T>;
private _makeComponent<T extends UnknownFunction>(
metadata: MockFunctionMetadata<T>,
metadata: MockMetadata<T>,
restore?: () => void,
):
| Record<string, any>
| Array<unknown>
| RegExp
| ReturnType<T>
| undefined
| Mock<T> {
): Record<string, any> | Array<unknown> | RegExp | T | Mock | undefined {
mrazauskas marked this conversation as resolved.
Show resolved Hide resolved
if (metadata.type === 'object') {
return new this._environmentGlobal.Object();
} else if (metadata.type === 'array') {
Expand Down Expand Up @@ -808,7 +805,7 @@ export class ModuleMocker {
}

private _createMockFunction<T extends UnknownFunction>(
metadata: MockFunctionMetadata<T>,
metadata: MockMetadata<T>,
mockConstructor: Function,
): Function {
let name = metadata.name;
Expand Down Expand Up @@ -862,19 +859,14 @@ export class ModuleMocker {
return createConstructor(mockConstructor);
}

private _generateMock<T extends UnknownFunction>(
metadata: MockFunctionMetadata<T>,
private _generateMock<T>(
metadata: MockMetadata<T>,
callbacks: Array<Function>,
refs: {
[key: string]:
| Record<string, any>
| Array<unknown>
| RegExp
| UnknownFunction
| undefined
| Mock<T>;
},
): Mock<T> {
refs: Record<
number,
Record<string, any> | Array<unknown> | RegExp | T | Mock | undefined
>,
): Mocked<T> {
// metadata not compatible but it's the same type, maybe problem with
// overloading of _makeComponent and not _generateMock?
// @ts-expect-error - unsure why TSC complains here?
Expand Down Expand Up @@ -905,20 +897,18 @@ export class ModuleMocker {
mock.prototype.constructor = mock;
}

return mock as Mock<T>;
return mock as Mocked<T>;
}

/**
* @see README.md
* @param metadata Metadata for the mock in the schema returned by the
* getMetadata method of this module.
*/
generateFromMetadata<T extends UnknownFunction>(
metadata: MockFunctionMetadata<T>,
): Mock<T> {
generateFromMetadata<T>(metadata: MockMetadata<T>): Mocked<T> {
const callbacks: Array<Function> = [];
const refs = {};
const mock = this._generateMock(metadata, callbacks, refs);
const mock = this._generateMock<T>(metadata, callbacks, refs);
callbacks.forEach(setter => setter());
return mock;
}
Expand All @@ -927,11 +917,11 @@ export class ModuleMocker {
* @see README.md
* @param component The component for which to retrieve metadata.
*/
getMetadata<T extends UnknownFunction>(
component: ReturnType<T>,
_refs?: Map<ReturnType<T>, number>,
): MockFunctionMetadata<T> | null {
const refs = _refs || new Map<ReturnType<T>, number>();
getMetadata<T = unknown>(
component: T,
_refs?: Map<T, number>,
): MockMetadata<T> | null {
const refs = _refs || new Map<T, number>();
const ref = refs.get(component);
if (ref != null) {
return {ref};
Expand All @@ -942,7 +932,7 @@ export class ModuleMocker {
return null;
}

const metadata: MockFunctionMetadata<T> = {type};
const metadata: MockMetadata<T> = {type};
if (
type === 'constant' ||
type === 'collection' ||
Expand All @@ -967,7 +957,7 @@ export class ModuleMocker {
refs.set(component, metadata.refID);

let members: {
[key: string]: MockFunctionMetadata<T>;
[key: string]: MockMetadata<T>;
} | null = null;
// Leave arrays alone
if (type !== 'array') {
Expand Down Expand Up @@ -1007,7 +997,7 @@ export class ModuleMocker {
): fn is Mock<(...args: P) => R>;
isMockFunction(fn: unknown): fn is Mock<UnknownFunction>;
isMockFunction(fn: unknown): fn is Mock<UnknownFunction> {
return fn != null && (fn as any)._isMockFunction === true;
return fn != null && (fn as Mock)._isMockFunction === true;
}

fn<T extends FunctionLike = UnknownFunction>(implementation?: T): Mock<T> {
Expand Down
8 changes: 4 additions & 4 deletions packages/jest-runtime/src/index.ts
Expand Up @@ -50,7 +50,7 @@ import {
import type {Config, Global} from '@jest/types';
import HasteMap, {IModuleMap} from 'jest-haste-map';
import {formatStackTrace, separateMessageFromStack} from 'jest-message-util';
import type {MockFunctionMetadata, ModuleMocker} from 'jest-mock';
import type {MockMetadata, ModuleMocker} from 'jest-mock';
import {escapePathForRegex} from 'jest-regex-util';
import Resolver, {ResolveModuleConfig} from 'jest-resolve';
import {EXTENSION as SnapshotExtension} from 'jest-snapshot';
Expand Down Expand Up @@ -168,7 +168,7 @@ export default class Runtime {
private _isCurrentlyExecutingManualMock: string | null;
private _mainModule: Module | null;
private readonly _mockFactories: Map<string, () => unknown>;
private readonly _mockMetaDataCache: Map<string, MockFunctionMetadata>;
private readonly _mockMetaDataCache: Map<string, MockMetadata<any>>;
SimenB marked this conversation as resolved.
Show resolved Hide resolved
private _mockRegistry: Map<string, any>;
private _isolatedMockRegistry: Map<string, any> | null;
Comment on lines -171 to 173
Copy link
Contributor Author

@mrazauskas mrazauskas Sep 3, 2022

Choose a reason for hiding this comment

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

Ideally these three anys should be some T (a type of some mock module). unknown does not work unfortunately, because T cannot be assigned to unknown. The T could be assigned to T, but there is no way to have it here. Tricky indeed.

At the same time, here the shape of the mock is not important at all. Hence any looked fine. Hm.. I will try one more time.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Nope. No luck. It might make sense to create type MockModule = any and to use it here instead of any. Looks redundant, but perhaps that way this is more clear?

Copy link
Member

Choose a reason for hiding this comment

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

nah, current approach is fine 👍

private _moduleMockRegistry: Map<string, VMModule>;
Expand Down Expand Up @@ -1710,7 +1710,7 @@ export default class Runtime {
return Module;
}

private _generateMock(from: string, moduleName: string) {
private _generateMock<T>(from: string, moduleName: string) {
const modulePath =
this._resolver.resolveStubModuleName(from, moduleName) ||
this._resolveCjsModule(from, moduleName);
Expand Down Expand Up @@ -1747,7 +1747,7 @@ export default class Runtime {
}
this._mockMetaDataCache.set(modulePath, mockMetadata);
}
return this._moduleMocker.generateFromMetadata(
return this._moduleMocker.generateFromMetadata<T>(
// added above if missing
this._mockMetaDataCache.get(modulePath)!,
);
Expand Down