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
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

### Features

- `[@jest/environment, jest-runtime]` Allow passing a generic type argument to `jest.createMockFromModule<T>()` method ([#13202](https://github.com/facebook/jest/pull/13202))

### Fixes

### Chore & Maintenance
Expand Down
52 changes: 39 additions & 13 deletions docs/JestObjectAPI.md
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
8 changes: 4 additions & 4 deletions packages/jest-runtime/src/index.ts
Original file line number Diff line number Diff line change
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
8 changes: 8 additions & 0 deletions packages/jest-types/__typetests__/jest.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,15 @@ expectError(jest.autoMockOff(true));
expectType<typeof jest>(jest.autoMockOn());
expectError(jest.autoMockOn(false));

const someModule = {
methodA: () => {},
propertyB: 'B',
};

expectType<unknown>(jest.createMockFromModule('moduleName'));
expectType<Mocked<typeof someModule>>(
jest.createMockFromModule<typeof someModule>('moduleName'),
);
Comment on lines +71 to +73
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Just a minimal test, because the serious ones landed in #13207

expectError(jest.createMockFromModule());

expectType<typeof jest>(jest.deepUnmock('moduleName'));
Expand Down