Skip to content

Commit cbf4676

Browse files
committed
feat(runtime): add jest.mockModule
1 parent e651a21 commit cbf4676

File tree

5 files changed

+201
-22
lines changed

5 files changed

+201
-22
lines changed

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

+1-1
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ Ran all test suites matching /native-esm.tla.test.js/i.
1010
1111
exports[`on node ^12.16.0 || >=13.7.0 runs test with native ESM 1`] = `
1212
Test Suites: 1 passed, 1 total
13-
Tests: 18 passed, 18 total
13+
Tests: 19 passed, 19 total
1414
Snapshots: 0 total
1515
Time: <<REPLACED>>
1616
Ran all test suites matching /native-esm.test.js/i.

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

+11
Original file line numberDiff line numberDiff line change
@@ -161,3 +161,14 @@ test('handle circular dependency', async () => {
161161
expect(moduleA.moduleB.id).toBe('circularDependentB');
162162
expect(moduleA.moduleB.moduleA).toBe(moduleA);
163163
});
164+
165+
test('can mock module', async () => {
166+
jestObject.mockModule('../mockedModule.mjs', () => ({foo: 'bar'}), {
167+
virtual: true,
168+
});
169+
170+
const importedMock = await import('../mockedModule.mjs');
171+
172+
expect(Object.keys(importedMock)).toEqual(['foo']);
173+
expect(importedMock.foo).toEqual('bar');
174+
});

packages/jest-environment/src/index.ts

+8
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,14 @@ export interface Jest {
146146
moduleFactory?: () => unknown,
147147
options?: {virtual?: boolean},
148148
): Jest;
149+
/**
150+
* Mocks a module with an auto-mocked version when it is being required.
151+
*/
152+
mockModule(
153+
moduleName: string,
154+
moduleFactory?: () => Promise<unknown> | unknown,
155+
options?: {virtual?: boolean},
156+
): Jest;
149157
/**
150158
* Returns the actual module instead of a mock, bypassing all checks on
151159
* whether the module should receive a mock implementation or not.

packages/jest-resolve/src/index.ts

+1-3
Original file line numberDiff line numberDiff line change
@@ -313,10 +313,8 @@ class Resolver {
313313
getModuleID(
314314
virtualMocks: Map<string, boolean>,
315315
from: Config.Path,
316-
_moduleName?: string,
316+
moduleName = '',
317317
): string {
318-
const moduleName = _moduleName || '';
319-
320318
const key = from + path.delimiter + moduleName;
321319
const cachedModuleID = this._moduleIDCache.get(key);
322320
if (cachedModuleID) {

packages/jest-runtime/src/index.ts

+180-18
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,7 @@ export default class Runtime {
148148
private _currentlyExecutingModulePath: string;
149149
private readonly _environment: JestEnvironment;
150150
private readonly _explicitShouldMock: Map<string, boolean>;
151+
private readonly _explicitShouldMockModule: Map<string, boolean>;
151152
private _fakeTimersImplementation:
152153
| LegacyFakeTimers<unknown>
153154
| ModernFakeTimers
@@ -162,6 +163,8 @@ export default class Runtime {
162163
>;
163164
private _mockRegistry: Map<string, any>;
164165
private _isolatedMockRegistry: Map<string, any> | null;
166+
private _moduleMockRegistry: Map<string, VMModule>;
167+
private readonly _moduleMockFactories: Map<string, () => unknown>;
165168
private readonly _moduleMocker: ModuleMocker;
166169
private _isolatedModuleRegistry: ModuleRegistry | null;
167170
private _moduleRegistry: ModuleRegistry;
@@ -182,6 +185,7 @@ export default class Runtime {
182185
private readonly _transitiveShouldMock: Map<string, boolean>;
183186
private _unmockList: RegExp | undefined;
184187
private readonly _virtualMocks: Map<string, boolean>;
188+
private readonly _virtualModuleMocks: Map<string, boolean>;
185189
private _moduleImplementation?: typeof nativeModule.Module;
186190
private readonly jestObjectCaches: Map<string, Jest>;
187191
private jestGlobals?: JestGlobals;
@@ -200,11 +204,14 @@ export default class Runtime {
200204
this._currentlyExecutingModulePath = '';
201205
this._environment = environment;
202206
this._explicitShouldMock = new Map();
207+
this._explicitShouldMockModule = new Map();
203208
this._internalModuleRegistry = new Map();
204209
this._isCurrentlyExecutingManualMock = null;
205210
this._mainModule = null;
206211
this._mockFactories = new Map();
207212
this._mockRegistry = new Map();
213+
this._moduleMockRegistry = new Map();
214+
this._moduleMockFactories = new Map();
208215
invariant(
209216
this._environment.moduleMocker,
210217
'`moduleMocker` must be set on an environment when created',
@@ -221,6 +228,7 @@ export default class Runtime {
221228
this._sourceMapRegistry = new Map();
222229
this._fileTransforms = new Map();
223230
this._virtualMocks = new Map();
231+
this._virtualModuleMocks = new Map();
224232
this.jestObjectCaches = new Map();
225233

226234
this._mockMetaDataCache = new Map();
@@ -488,6 +496,16 @@ export default class Runtime {
488496

489497
const [path, query] = specifier.split('?');
490498

499+
if (
500+
this._shouldMock(
501+
referencingIdentifier,
502+
path,
503+
this._explicitShouldMockModule,
504+
)
505+
) {
506+
return this.importMock(referencingIdentifier, path, context);
507+
}
508+
491509
const resolved = this._resolveModule(referencingIdentifier, path);
492510

493511
if (
@@ -503,6 +521,8 @@ export default class Runtime {
503521
async unstable_importModule(
504522
from: Config.Path,
505523
moduleName?: string,
524+
// TODO: implement this
525+
_isImportActual = false,
506526
): Promise<void> {
507527
invariant(
508528
runtimeSupportsVmModules,
@@ -556,11 +576,114 @@ export default class Runtime {
556576
return evaluateSyntheticModule(module);
557577
}
558578

579+
private async importMock<T = unknown>(
580+
from: Config.Path,
581+
moduleName: string,
582+
context: VMContext,
583+
): Promise<T> {
584+
const moduleID = this._resolver.getModuleID(
585+
this._virtualModuleMocks,
586+
from,
587+
moduleName,
588+
);
589+
590+
if (this._moduleMockRegistry.has(moduleID)) {
591+
return this._moduleMockRegistry.get(moduleID);
592+
}
593+
594+
if (this._moduleMockFactories.has(moduleID)) {
595+
const invokedFactory: any = await this._moduleMockFactories.get(
596+
moduleID,
597+
// has check above makes this ok
598+
)!();
599+
600+
const module = new SyntheticModule(
601+
Object.keys(invokedFactory),
602+
function () {
603+
Object.entries(invokedFactory).forEach(([key, value]) => {
604+
// @ts-expect-error: TS doesn't know what `this` is
605+
this.setExport(key, value);
606+
});
607+
},
608+
// should identifier be `node://${moduleName}`?
609+
{context, identifier: moduleName},
610+
);
611+
612+
this._moduleMockRegistry.set(moduleID, module);
613+
614+
return evaluateSyntheticModule(module);
615+
}
616+
617+
const manualMockOrStub = this._resolver.getMockModule(from, moduleName);
618+
619+
let modulePath =
620+
this._resolver.getMockModule(from, moduleName) ||
621+
this._resolveModule(from, moduleName);
622+
623+
let isManualMock =
624+
manualMockOrStub &&
625+
!this._resolver.resolveStubModuleName(from, moduleName);
626+
if (!isManualMock) {
627+
// If the actual module file has a __mocks__ dir sitting immediately next
628+
// to it, look to see if there is a manual mock for this file.
629+
//
630+
// subDir1/my_module.js
631+
// subDir1/__mocks__/my_module.js
632+
// subDir2/my_module.js
633+
// subDir2/__mocks__/my_module.js
634+
//
635+
// Where some other module does a relative require into each of the
636+
// respective subDir{1,2} directories and expects a manual mock
637+
// corresponding to that particular my_module.js file.
638+
639+
const moduleDir = path.dirname(modulePath);
640+
const moduleFileName = path.basename(modulePath);
641+
const potentialManualMock = path.join(
642+
moduleDir,
643+
'__mocks__',
644+
moduleFileName,
645+
);
646+
if (fs.existsSync(potentialManualMock)) {
647+
isManualMock = true;
648+
modulePath = potentialManualMock;
649+
}
650+
}
651+
if (isManualMock) {
652+
const localModule: InitialModule = {
653+
children: [],
654+
exports: {},
655+
filename: modulePath,
656+
id: modulePath,
657+
loaded: false,
658+
path: modulePath,
659+
};
660+
661+
this._loadModule(
662+
localModule,
663+
from,
664+
moduleName,
665+
modulePath,
666+
undefined,
667+
this._moduleMockRegistry,
668+
);
669+
670+
this._moduleMockRegistry.set(moduleID, localModule.exports);
671+
} else {
672+
// Look for a real module to generate an automock from
673+
this._moduleMockRegistry.set(
674+
moduleID,
675+
this._generateMock(from, moduleName),
676+
);
677+
}
678+
679+
return this._moduleMockRegistry.get(moduleID);
680+
}
681+
559682
requireModule<T = unknown>(
560683
from: Config.Path,
561684
moduleName?: string,
562685
options?: InternalModuleOptions,
563-
isRequireActual?: boolean | null,
686+
isRequireActual = false,
564687
): T {
565688
const moduleID = this._resolver.getModuleID(
566689
this._virtualMocks,
@@ -597,12 +720,10 @@ export default class Runtime {
597720

598721
if (options?.isInternalModule) {
599722
moduleRegistry = this._internalModuleRegistry;
723+
} else if (this._isolatedModuleRegistry) {
724+
moduleRegistry = this._isolatedModuleRegistry;
600725
} else {
601-
if (this._isolatedModuleRegistry) {
602-
moduleRegistry = this._isolatedModuleRegistry;
603-
} else {
604-
moduleRegistry = this._moduleRegistry;
605-
}
726+
moduleRegistry = this._moduleRegistry;
606727
}
607728

608729
const module = moduleRegistry.get(modulePath);
@@ -663,17 +784,12 @@ export default class Runtime {
663784
moduleName,
664785
);
665786

666-
if (
667-
this._isolatedMockRegistry &&
668-
this._isolatedMockRegistry.get(moduleID)
669-
) {
670-
return this._isolatedMockRegistry.get(moduleID);
671-
} else if (this._mockRegistry.get(moduleID)) {
672-
return this._mockRegistry.get(moduleID);
673-
}
674-
675787
const mockRegistry = this._isolatedMockRegistry || this._mockRegistry;
676788

789+
if (mockRegistry.get(moduleID)) {
790+
return mockRegistry.get(moduleID);
791+
}
792+
677793
if (this._mockFactories.has(moduleID)) {
678794
// has check above makes this ok
679795
const module = this._mockFactories.get(moduleID)!();
@@ -790,7 +906,7 @@ export default class Runtime {
790906
}
791907

792908
try {
793-
if (this._shouldMock(from, moduleName)) {
909+
if (this._shouldMock(from, moduleName, this._explicitShouldMock)) {
794910
return this.requireMock<T>(from, moduleName);
795911
} else {
796912
return this.requireModule<T>(from, moduleName);
@@ -845,6 +961,7 @@ export default class Runtime {
845961
this._mockRegistry.clear();
846962
this._moduleRegistry.clear();
847963
this._esmoduleRegistry.clear();
964+
this._moduleMockRegistry.clear();
848965

849966
if (this._environment) {
850967
if (this._environment.global) {
@@ -933,6 +1050,26 @@ export default class Runtime {
9331050
this._mockFactories.set(moduleID, mockFactory);
9341051
}
9351052

1053+
private setModuleMock(
1054+
from: string,
1055+
moduleName: string,
1056+
mockFactory: () => Promise<unknown> | unknown,
1057+
options?: {virtual?: boolean},
1058+
): void {
1059+
if (options?.virtual) {
1060+
const mockPath = this._resolver.getModulePath(from, moduleName);
1061+
1062+
this._virtualModuleMocks.set(mockPath, true);
1063+
}
1064+
const moduleID = this._resolver.getModuleID(
1065+
this._virtualModuleMocks,
1066+
from,
1067+
moduleName,
1068+
);
1069+
this._explicitShouldMockModule.set(moduleID, true);
1070+
this._moduleMockFactories.set(moduleID, mockFactory);
1071+
}
1072+
9361073
restoreAllMocks(): void {
9371074
this._moduleMocker.restoreAllMocks();
9381075
}
@@ -953,12 +1090,15 @@ export default class Runtime {
9531090
this._internalModuleRegistry.clear();
9541091
this._mainModule = null;
9551092
this._mockFactories.clear();
1093+
this._moduleMockFactories.clear();
9561094
this._mockMetaDataCache.clear();
9571095
this._shouldMockModuleCache.clear();
9581096
this._shouldUnmockTransitiveDependenciesCache.clear();
9591097
this._explicitShouldMock.clear();
1098+
this._explicitShouldMockModule.clear();
9601099
this._transitiveShouldMock.clear();
9611100
this._virtualMocks.clear();
1101+
this._virtualModuleMocks.clear();
9621102
this._cacheFS.clear();
9631103
this._unmockList = undefined;
9641104

@@ -1350,8 +1490,11 @@ export default class Runtime {
13501490
);
13511491
}
13521492

1353-
private _shouldMock(from: Config.Path, moduleName: string): boolean {
1354-
const explicitShouldMock = this._explicitShouldMock;
1493+
private _shouldMock(
1494+
from: Config.Path,
1495+
moduleName: string,
1496+
explicitShouldMock: Map<string, boolean>,
1497+
): boolean {
13551498
const moduleID = this._resolver.getModuleID(
13561499
this._virtualMocks,
13571500
from,
@@ -1519,6 +1662,24 @@ export default class Runtime {
15191662
this.setMock(from, moduleName, mockFactory, options);
15201663
return jestObject;
15211664
};
1665+
const mockModule: Jest['mockModule'] = (
1666+
moduleName,
1667+
mockFactory,
1668+
options,
1669+
) => {
1670+
if (mockFactory !== undefined) {
1671+
this.setModuleMock(from, moduleName, mockFactory, options);
1672+
return jestObject;
1673+
}
1674+
1675+
const moduleID = this._resolver.getModuleID(
1676+
this._virtualMocks,
1677+
from,
1678+
moduleName,
1679+
);
1680+
this._explicitShouldMockModule.set(moduleID, true);
1681+
return jestObject;
1682+
};
15221683
const clearAllMocks = () => {
15231684
this.clearAllMocks();
15241685
return jestObject;
@@ -1617,6 +1778,7 @@ export default class Runtime {
16171778
isMockFunction: this._moduleMocker.isMockFunction,
16181779
isolateModules,
16191780
mock,
1781+
mockModule,
16201782
requireActual: this.requireActual.bind(this, from),
16211783
requireMock: this.requireMock.bind(this, from),
16221784
resetAllMocks,

0 commit comments

Comments
 (0)