Skip to content

Commit 9c9ce8a

Browse files
feat: add support unstable_unmockModule (#15080)
1 parent d65d4cc commit 9c9ce8a

File tree

9 files changed

+140
-21
lines changed

9 files changed

+140
-21
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
- `[jest-runtime]` Support `import.meta.filename` and `import.meta.dirname` (available from [Node 20.11](https://nodejs.org/en/blog/release/v20.11.0)) ([#14854](https://github.com/jestjs/jest/pull/14854))
3131
- `[jest-runtime]` Support `import.meta.resolve` ([#14930](https://github.com/jestjs/jest/pull/14930))
3232
- `[jest-runtime]` [**BREAKING**] Make it mandatory to pass `globalConfig` to the `Runtime` constructor ([#15044](https://github.com/jestjs/jest/pull/15044))
33+
- `[jest-runtime]` Add `unstable_unmockModule` ([#15080](https://github.com/jestjs/jest/pull/15080))
3334
- `[@jest/schemas]` Upgrade `@sinclair/typebox` to v0.31 ([#14072](https://github.com/jestjs/jest/pull/14072))
3435
- `[@jest/types]` `test.each()`: Accept a readonly (`as const`) table properly ([#14565](https://github.com/jestjs/jest/pull/14565))
3536
- `[@jest/types]` Improve argument type inference passed to `test` and `describe` callback functions from `each` tables ([#14920](https://github.com/jestjs/jest/pull/14920))

docs/ECMAScriptModules.md

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,54 @@ const {execSync} = await import('node:child_process');
6565
// etc.
6666
```
6767

68+
## Module unmocking in ESM
69+
70+
```js title="esm-module.mjs"
71+
export default () => {
72+
return 'default';
73+
};
74+
75+
export const namedFn = () => {
76+
return 'namedFn';
77+
};
78+
```
79+
80+
```js title="esm-module.test.mjs"
81+
import {jest, test} from '@jest/globals';
82+
83+
test('test esm-module', async () => {
84+
jest.unstable_mockModule('./esm-module.js', () => ({
85+
default: () => 'default implementation',
86+
namedFn: () => 'namedFn implementation',
87+
}));
88+
89+
const mockModule = await import('./esm-module.js');
90+
91+
console.log(mockModule.default()); // 'default implementation'
92+
console.log(mockModule.namedFn()); // 'namedFn implementation'
93+
94+
jest.unstable_unmockModule('./esm-module.js');
95+
96+
const originalModule = await import('./esm-module.js');
97+
98+
console.log(originalModule.default()); // 'default'
99+
console.log(originalModule.namedFn()); // 'namedFn'
100+
101+
/* !!! WARNING !!! Don`t override */
102+
jest.unstable_mockModule('./esm-module.js', () => ({
103+
default: () => 'default override implementation',
104+
namedFn: () => 'namedFn override implementation',
105+
}));
106+
107+
const mockModuleOverride = await import('./esm-module.js');
108+
109+
console.log(mockModuleOverride.default()); // 'default implementation'
110+
console.log(mockModuleOverride.namedFn()); // 'namedFn implementation'
111+
});
112+
```
113+
114+
## Mocking CJS modules
115+
68116
For mocking CJS modules, you should continue to use `jest.mock`. See the example below:
69117

70118
```js title="main.cjs"

e2e/__tests__/__snapshots__/nativeEsm.test.ts.snap

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,12 +42,20 @@ Ran all test suites matching native-esm-wasm.test.js."
4242
4343
exports[`runs test with native ESM 1`] = `
4444
"Test Suites: 1 passed, 1 total
45-
Tests: 33 passed, 33 total
45+
Tests: 31 passed, 31 total
4646
Snapshots: 0 total
4747
Time: <<REPLACED>>
4848
Ran all test suites matching native-esm.test.js."
4949
`;
5050
51+
exports[`runs test with native mock ESM 1`] = `
52+
"Test Suites: 1 passed, 1 total
53+
Tests: 3 passed, 3 total
54+
Snapshots: 0 total
55+
Time: <<REPLACED>>
56+
Ran all test suites matching native-esm-mocks.test.js."
57+
`;
58+
5159
exports[`support re-exports from CJS of core module 1`] = `
5260
"Test Suites: 1 passed, 1 total
5361
Tests: 1 passed, 1 total

e2e/__tests__/nativeEsm.test.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,22 @@ test('runs test with native ESM', () => {
5151
expect(exitCode).toBe(0);
5252
});
5353

54+
test('runs test with native mock ESM', () => {
55+
const {exitCode, stderr, stdout} = runJest(
56+
DIR,
57+
['native-esm-mocks.test.js'],
58+
{
59+
nodeOptions: '--experimental-vm-modules --no-warnings',
60+
},
61+
);
62+
63+
const {summary} = extractSummary(stderr);
64+
65+
expect(summary).toMatchSnapshot();
66+
expect(stdout).toBe('');
67+
expect(exitCode).toBe(0);
68+
});
69+
5470
test('supports top-level await', () => {
5571
const {exitCode, stderr, stdout} = runJest(DIR, ['native-esm.tla.test.js'], {
5672
nodeOptions: '--experimental-vm-modules --no-warnings',
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
/**
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*/
7+
import {jest as jestObject} from '@jest/globals';
8+
9+
afterEach(() => {
10+
jestObject.resetModules();
11+
});
12+
13+
test('can mock module', async () => {
14+
jestObject.unstable_mockModule('../mockedModule.mjs', () => ({foo: 'bar'}), {
15+
virtual: true,
16+
});
17+
18+
const importedMock = await import('../mockedModule.mjs');
19+
20+
expect(Object.keys(importedMock)).toEqual(['foo']);
21+
expect(importedMock.foo).toBe('bar');
22+
});
23+
24+
test('can mock transitive module', async () => {
25+
jestObject.unstable_mockModule('../index.js', () => ({foo: 'bar'}));
26+
27+
const importedMock = await import('../reexport.js');
28+
29+
expect(Object.keys(importedMock)).toEqual(['foo']);
30+
expect(importedMock.foo).toBe('bar');
31+
});
32+
33+
test('can unmock module', async () => {
34+
jestObject.unstable_mockModule('../index.js', () => ({
35+
double: () => 1000,
36+
}));
37+
38+
const importedMock = await import('../index.js');
39+
expect(importedMock.double()).toBe(1000);
40+
41+
jestObject.unstable_unmockModule('../index.js');
42+
43+
const importedMockAfterUnmock = await import('../index.js');
44+
expect(importedMockAfterUnmock.double(2)).toBe(4);
45+
});

e2e/native-esm/__tests__/native-esm.test.js

Lines changed: 0 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -224,26 +224,6 @@ test('require of ESM should throw correct error', () => {
224224
);
225225
});
226226

227-
test('can mock module', async () => {
228-
jestObject.unstable_mockModule('../mockedModule.mjs', () => ({foo: 'bar'}), {
229-
virtual: true,
230-
});
231-
232-
const importedMock = await import('../mockedModule.mjs');
233-
234-
expect(Object.keys(importedMock)).toEqual(['foo']);
235-
expect(importedMock.foo).toBe('bar');
236-
});
237-
238-
test('can mock transitive module', async () => {
239-
jestObject.unstable_mockModule('../index.js', () => ({foo: 'bar'}));
240-
241-
const importedMock = await import('../reexport.js');
242-
243-
expect(Object.keys(importedMock)).toEqual(['foo']);
244-
expect(importedMock.foo).toBe('bar');
245-
});
246-
247227
test('supports imports using "node:" prefix', () => {
248228
expect(dns).toBe(prefixDns);
249229
});

packages/jest-environment/src/index.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -392,6 +392,12 @@ export interface Jest {
392392
* real module).
393393
*/
394394
unmock(moduleName: string): Jest;
395+
/**
396+
* Indicates that the module system should never return a mocked version of
397+
* the specified module when it is being imported (e.g. that it should always
398+
* return the real module).
399+
*/
400+
unstable_unmockModule(moduleName: string): Jest;
395401
/**
396402
* Instructs Jest to use fake versions of the global date, performance,
397403
* time and timer APIs. Fake timers implementation is backed by

packages/jest-runtime/src/index.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2159,6 +2159,16 @@ export default class Runtime {
21592159
this._explicitShouldMock.set(moduleID, false);
21602160
return jestObject;
21612161
};
2162+
const unmockModule = (moduleName: string) => {
2163+
const moduleID = this._resolver.getModuleID(
2164+
this._virtualModuleMocks,
2165+
from,
2166+
moduleName,
2167+
{conditions: this.esmConditions},
2168+
);
2169+
this._explicitShouldMockModule.set(moduleID, false);
2170+
return jestObject;
2171+
};
21622172
const deepUnmock = (moduleName: string) => {
21632173
const moduleID = this._resolver.getModuleID(
21642174
this._virtualMocks,
@@ -2414,6 +2424,7 @@ export default class Runtime {
24142424
spyOn,
24152425
unmock,
24162426
unstable_mockModule: mockModule,
2427+
unstable_unmockModule: unmockModule,
24172428
useFakeTimers,
24182429
useRealTimers,
24192430
};

packages/jest-types/__typetests__/jest.test.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ expect(
5151
.setMock('moduleName', {a: 'b'})
5252
.setTimeout(6000)
5353
.unmock('moduleName')
54+
.unstable_unmockModule('moduleName')
5455
.useFakeTimers()
5556
.useFakeTimers({legacyFakeTimers: true})
5657
.useRealTimers(),
@@ -183,6 +184,9 @@ expect(jest.setMock('moduleName')).type.toRaiseError();
183184
expect(jest.unmock('moduleName')).type.toBe<typeof jest>();
184185
expect(jest.unmock()).type.toRaiseError();
185186

187+
expect(jest.unstable_unmockModule('moduleName')).type.toBe<typeof jest>();
188+
expect(jest.unstable_unmockModule()).type.toRaiseError();
189+
186190
// Mock Functions
187191

188192
expect(jest.clearAllMocks()).type.toBe<typeof jest>();

0 commit comments

Comments
 (0)