Skip to content

Commit b51c79d

Browse files
committed
feat: add onGenerateMock transformer callback (#15429)
1 parent 611d1a4 commit b51c79d

File tree

5 files changed

+154
-1
lines changed

5 files changed

+154
-1
lines changed

CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@
4646
- `[@jest/util-snapshot]` Extract utils used by tooling from `jest-snapshot` into its own package ([#15095](https://github.com/facebook/jest/pull/15095))
4747
- `[pretty-format]` [**BREAKING**] Do not render empty string children (`''`) in React plugin ([#14470](https://github.com/facebook/jest/pull/14470))
4848
- `[jest-each]` Introduce `%$` option to add number of the test to its title ([#14710](https://github.com/jestjs/jest/pull/14710))
49+
- `[jest-runtime]` Add `onGenerateMock` transformer callback for auto generated callbacks ([#15433](https://github.com/jestjs/jest/pull/15433))
4950

5051
### Fixes
5152

docs/JestObjectAPI.md

+45
Original file line numberDiff line numberDiff line change
@@ -528,6 +528,51 @@ getRandom(); // Always returns 10
528528

529529
Returns a mock module instead of the actual module, bypassing all checks on whether the module should be required normally or not.
530530

531+
### `jest.onGenerateMock(cb)`
532+
533+
Registers a callback function that is invoked whenever Jest generates a mock for a module.
534+
This callback allows you to modify the mock before it is returned to the rest of your tests.
535+
536+
Parameters for callback:
537+
1. `moduleName: string` - The name of the module that is being mocked.
538+
2. `moduleMock: T` - The mock object that Jest has generated for the module. This object can be modified or replaced before returning.
539+
540+
Behaviour:
541+
542+
- If multiple callbacks are registered via consecutive `onGenerateMock` calls, they will be invoked **in the order they were added**.
543+
- Each callback receives the output of the previous callback as its `moduleMock`. This makes it possible to apply multiple layers of transformations to the same mock.
544+
545+
```js
546+
jest.onGenerateMock((moduleName, moduleMock) => {
547+
// Inspect the module name and decide how to transform the mock
548+
if (moduleName.includes('Database')) {
549+
// For demonstration, let's replace a method with our own custom mock
550+
moduleMock.connect = jest.fn().mockImplementation(() => {
551+
console.log('Connected to mock DB');
552+
});
553+
}
554+
555+
// Return the (potentially modified) mock
556+
return moduleMock;
557+
});
558+
559+
// Apply mock for module
560+
jest.mock('./Database');
561+
562+
// Later in your tests
563+
import Database from './Database';
564+
// The `Database` mock now has any transformations applied by our callback
565+
```
566+
567+
:::note
568+
569+
The `onGenerateMock` callback is not called for manually created mocks, such as:
570+
571+
- Mocks defined in a `__mocks__` folder
572+
- Explicit factories provided via `jest.mock('moduleName', () => { ... })`
573+
574+
:::
575+
531576
### `jest.resetModules()`
532577

533578
Resets the module registry - the cache of all required modules. This is useful to isolate modules where local state might conflict between tests.

packages/jest-environment/src/index.ts

+12
Original file line numberDiff line numberDiff line change
@@ -207,6 +207,18 @@ export interface Jest {
207207
moduleFactory?: () => T,
208208
options?: {virtual?: boolean},
209209
): Jest;
210+
/**
211+
* Registers a callback function that is invoked whenever a mock is generated for a module.
212+
* This callback is passed the module name and the newly created mock object, and must return
213+
* the (potentially modified) mock object.
214+
*
215+
* If multiple callbacks are registered, they will be called in the order they were added.
216+
* Each callback receives the result of the previous callback as the `moduleMock` parameter,
217+
* making it possible to apply sequential transformations.
218+
*
219+
* @param cb
220+
*/
221+
onGenerateMock<T>(cb: (moduleName: string, moduleMock: T) => T): Jest;
210222
/**
211223
* Mocks a module with the provided module factory when it is being imported.
212224
*/

packages/jest-runtime/src/__tests__/runtime_mock.test.js

+78
Original file line numberDiff line numberDiff line change
@@ -136,4 +136,82 @@ describe('Runtime', () => {
136136
).toBe(mockReference);
137137
});
138138
});
139+
140+
describe('jest.onGenerateMock', () => {
141+
it('calls single callback and returns transformed value', async () => {
142+
const runtime = await createRuntime(__filename);
143+
const mockReference = {isMock: true};
144+
const root = runtime.requireModule(runtime.__mockRootPath, rootJsPath);
145+
// Erase module registry because root.js requires most other modules.
146+
root.jest.resetModules();
147+
148+
const onGenerateMock = jest.fn((moduleName, moduleMock) => mockReference);
149+
150+
root.jest.onGenerateMock(onGenerateMock);
151+
root.jest.mock('RegularModule');
152+
root.jest.mock('ManuallyMocked');
153+
154+
expect(
155+
runtime.requireModuleOrMock(runtime.__mockRootPath, 'RegularModule'),
156+
).toEqual(mockReference);
157+
expect(onGenerateMock).toHaveBeenCalledWith(
158+
'RegularModule',
159+
expect.anything(),
160+
);
161+
162+
onGenerateMock.mockReset();
163+
164+
expect(
165+
runtime.requireModuleOrMock(runtime.__mockRootPath, 'ManuallyMocked'),
166+
).not.toEqual(mockReference);
167+
expect(onGenerateMock).not.toHaveBeenCalled();
168+
});
169+
170+
it('calls multiple callback and returns transformed value', async () => {
171+
const runtime = await createRuntime(__filename);
172+
const root = runtime.requireModule(runtime.__mockRootPath, rootJsPath);
173+
// Erase module registry because root.js requires most other modules.
174+
root.jest.resetModules();
175+
176+
const onGenerateMock1 = jest.fn((moduleName, moduleMock) => ({
177+
isMock: true,
178+
value: 1,
179+
}));
180+
181+
const onGenerateMock2 = jest.fn((moduleName, moduleMock) => ({
182+
...moduleMock,
183+
value: moduleMock.value + 1,
184+
}));
185+
186+
const onGenerateMock3 = jest.fn((moduleName, moduleMock) => ({
187+
...moduleMock,
188+
value: moduleMock.value ** 2,
189+
}));
190+
191+
root.jest.onGenerateMock(onGenerateMock1);
192+
root.jest.onGenerateMock(onGenerateMock2);
193+
root.jest.onGenerateMock(onGenerateMock3);
194+
root.jest.mock('RegularModule');
195+
root.jest.mock('ManuallyMocked');
196+
197+
expect(
198+
runtime.requireModuleOrMock(runtime.__mockRootPath, 'RegularModule'),
199+
).toEqual({
200+
isMock: true,
201+
value: 4,
202+
});
203+
expect(onGenerateMock1).toHaveBeenCalledWith(
204+
'RegularModule',
205+
expect.anything(),
206+
);
207+
expect(onGenerateMock2).toHaveBeenCalledWith('RegularModule', {
208+
isMock: true,
209+
value: 1,
210+
});
211+
expect(onGenerateMock3).toHaveBeenCalledWith('RegularModule', {
212+
isMock: true,
213+
value: 2,
214+
});
215+
});
216+
});
139217
});

packages/jest-runtime/src/index.ts

+18-1
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,9 @@ export default class Runtime {
171171
private readonly _environment: JestEnvironment;
172172
private readonly _explicitShouldMock: Map<string, boolean>;
173173
private readonly _explicitShouldMockModule: Map<string, boolean>;
174+
private readonly _onGenerateMock: Set<
175+
(moduleName: string, moduleMock: any) => any
176+
>;
174177
private _fakeTimersImplementation:
175178
| LegacyFakeTimers<unknown>
176179
| ModernFakeTimers
@@ -235,6 +238,7 @@ export default class Runtime {
235238
this._globalConfig = globalConfig;
236239
this._explicitShouldMock = new Map();
237240
this._explicitShouldMockModule = new Map();
241+
this._onGenerateMock = new Set();
238242
this._internalModuleRegistry = new Map();
239243
this._isCurrentlyExecutingManualMock = null;
240244
this._mainModule = null;
@@ -1930,10 +1934,16 @@ export default class Runtime {
19301934
}
19311935
this._mockMetaDataCache.set(modulePath, mockMetadata);
19321936
}
1933-
return this._moduleMocker.generateFromMetadata<T>(
1937+
let moduleMock = this._moduleMocker.generateFromMetadata<T>(
19341938
// added above if missing
19351939
this._mockMetaDataCache.get(modulePath)!,
19361940
);
1941+
1942+
for (const onGenerateMock of this._onGenerateMock) {
1943+
moduleMock = onGenerateMock(moduleName, moduleMock);
1944+
}
1945+
1946+
return moduleMock;
19371947
}
19381948

19391949
private _shouldMockCjs(
@@ -2193,6 +2203,12 @@ export default class Runtime {
21932203
this._explicitShouldMock.set(moduleID, true);
21942204
return jestObject;
21952205
};
2206+
const onGenerateMock: Jest['onGenerateMock'] = <T>(
2207+
cb: (moduleName: string, moduleMock: T) => T,
2208+
) => {
2209+
this._onGenerateMock.add(cb);
2210+
return jestObject;
2211+
};
21962212
const setMockFactory = (
21972213
moduleName: string,
21982214
mockFactory: () => unknown,
@@ -2364,6 +2380,7 @@ export default class Runtime {
23642380
mock,
23652381
mocked,
23662382
now: () => _getFakeTimers().now(),
2383+
onGenerateMock,
23672384
replaceProperty,
23682385
requireActual: moduleName => this.requireActual(from, moduleName),
23692386
requireMock: moduleName => this.requireMock(from, moduleName),

0 commit comments

Comments
 (0)