Skip to content

Commit d7b60be

Browse files
committed
feat(runtime): add jest.mockModule
1 parent 5ba0cc9 commit d7b60be

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
@@ -166,3 +166,14 @@ test('handle circular dependency', async () => {
166166
expect(moduleA.moduleB.id).toBe('circularDependentB');
167167
expect(moduleA.moduleB.moduleA).toBe(moduleA);
168168
});
169+
170+
test('can mock module', async () => {
171+
jestObject.mockModule('../mockedModule.mjs', () => ({foo: 'bar'}), {
172+
virtual: true,
173+
});
174+
175+
const importedMock = await import('../mockedModule.mjs');
176+
177+
expect(Object.keys(importedMock)).toEqual(['foo']);
178+
expect(importedMock.foo).toEqual('bar');
179+
});

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;
@@ -183,6 +186,7 @@ export default class Runtime {
183186
private readonly _transitiveShouldMock: Map<string, boolean>;
184187
private _unmockList: RegExp | undefined;
185188
private readonly _virtualMocks: Map<string, boolean>;
189+
private readonly _virtualModuleMocks: Map<string, boolean>;
186190
private _moduleImplementation?: typeof nativeModule.Module;
187191
private readonly jestObjectCaches: Map<string, Jest>;
188192
private jestGlobals?: JestGlobals;
@@ -201,11 +205,14 @@ export default class Runtime {
201205
this._currentlyExecutingModulePath = '';
202206
this._environment = environment;
203207
this._explicitShouldMock = new Map();
208+
this._explicitShouldMockModule = new Map();
204209
this._internalModuleRegistry = new Map();
205210
this._isCurrentlyExecutingManualMock = null;
206211
this._mainModule = null;
207212
this._mockFactories = new Map();
208213
this._mockRegistry = new Map();
214+
this._moduleMockRegistry = new Map();
215+
this._moduleMockFactories = new Map();
209216
invariant(
210217
this._environment.moduleMocker,
211218
'`moduleMocker` must be set on an environment when created',
@@ -223,6 +230,7 @@ export default class Runtime {
223230
this._sourceMapRegistry = new Map();
224231
this._fileTransforms = new Map();
225232
this._virtualMocks = new Map();
233+
this._virtualModuleMocks = new Map();
226234
this.jestObjectCaches = new Map();
227235

228236
this._mockMetaDataCache = new Map();
@@ -490,6 +498,16 @@ export default class Runtime {
490498

491499
const [path, query] = specifier.split('?');
492500

501+
if (
502+
this._shouldMock(
503+
referencingIdentifier,
504+
path,
505+
this._explicitShouldMockModule,
506+
)
507+
) {
508+
return this.importMock(referencingIdentifier, path, context);
509+
}
510+
493511
const resolved = this._resolveModule(referencingIdentifier, path);
494512

495513
if (
@@ -505,6 +523,8 @@ export default class Runtime {
505523
async unstable_importModule(
506524
from: Config.Path,
507525
moduleName?: string,
526+
// TODO: implement this
527+
_isImportActual = false,
508528
): Promise<void> {
509529
invariant(
510530
runtimeSupportsVmModules,
@@ -552,6 +572,109 @@ export default class Runtime {
552572
return evaluateSyntheticModule(module);
553573
}
554574

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

@@ -583,7 +706,7 @@ export default class Runtime {
583706
from: Config.Path,
584707
moduleName?: string,
585708
options?: InternalModuleOptions,
586-
isRequireActual?: boolean | null,
709+
isRequireActual = false,
587710
): T {
588711
const moduleID = this._resolver.getModuleID(
589712
this._virtualMocks,
@@ -620,12 +743,10 @@ export default class Runtime {
620743

621744
if (options?.isInternalModule) {
622745
moduleRegistry = this._internalModuleRegistry;
746+
} else if (this._isolatedModuleRegistry) {
747+
moduleRegistry = this._isolatedModuleRegistry;
623748
} else {
624-
if (this._isolatedModuleRegistry) {
625-
moduleRegistry = this._isolatedModuleRegistry;
626-
} else {
627-
moduleRegistry = this._moduleRegistry;
628-
}
749+
moduleRegistry = this._moduleRegistry;
629750
}
630751

631752
const module = moduleRegistry.get(modulePath);
@@ -686,17 +807,12 @@ export default class Runtime {
686807
moduleName,
687808
);
688809

689-
if (
690-
this._isolatedMockRegistry &&
691-
this._isolatedMockRegistry.get(moduleID)
692-
) {
693-
return this._isolatedMockRegistry.get(moduleID);
694-
} else if (this._mockRegistry.get(moduleID)) {
695-
return this._mockRegistry.get(moduleID);
696-
}
697-
698810
const mockRegistry = this._isolatedMockRegistry || this._mockRegistry;
699811

812+
if (mockRegistry.get(moduleID)) {
813+
return mockRegistry.get(moduleID);
814+
}
815+
700816
if (this._mockFactories.has(moduleID)) {
701817
// has check above makes this ok
702818
const module = this._mockFactories.get(moduleID)!();
@@ -813,7 +929,7 @@ export default class Runtime {
813929
}
814930

815931
try {
816-
if (this._shouldMock(from, moduleName)) {
932+
if (this._shouldMock(from, moduleName, this._explicitShouldMock)) {
817933
return this.requireMock<T>(from, moduleName);
818934
} else {
819935
return this.requireModule<T>(from, moduleName);
@@ -869,6 +985,7 @@ export default class Runtime {
869985
this._moduleRegistry.clear();
870986
this._esmoduleRegistry.clear();
871987
this._cjsNamedExports.clear();
988+
this._moduleMockRegistry.clear();
872989

873990
if (this._environment) {
874991
if (this._environment.global) {
@@ -957,6 +1074,26 @@ export default class Runtime {
9571074
this._mockFactories.set(moduleID, mockFactory);
9581075
}
9591076

1077+
private setModuleMock(
1078+
from: string,
1079+
moduleName: string,
1080+
mockFactory: () => Promise<unknown> | unknown,
1081+
options?: {virtual?: boolean},
1082+
): void {
1083+
if (options?.virtual) {
1084+
const mockPath = this._resolver.getModulePath(from, moduleName);
1085+
1086+
this._virtualModuleMocks.set(mockPath, true);
1087+
}
1088+
const moduleID = this._resolver.getModuleID(
1089+
this._virtualModuleMocks,
1090+
from,
1091+
moduleName,
1092+
);
1093+
this._explicitShouldMockModule.set(moduleID, true);
1094+
this._moduleMockFactories.set(moduleID, mockFactory);
1095+
}
1096+
9601097
restoreAllMocks(): void {
9611098
this._moduleMocker.restoreAllMocks();
9621099
}
@@ -977,12 +1114,15 @@ export default class Runtime {
9771114
this._internalModuleRegistry.clear();
9781115
this._mainModule = null;
9791116
this._mockFactories.clear();
1117+
this._moduleMockFactories.clear();
9801118
this._mockMetaDataCache.clear();
9811119
this._shouldMockModuleCache.clear();
9821120
this._shouldUnmockTransitiveDependenciesCache.clear();
9831121
this._explicitShouldMock.clear();
1122+
this._explicitShouldMockModule.clear();
9841123
this._transitiveShouldMock.clear();
9851124
this._virtualMocks.clear();
1125+
this._virtualModuleMocks.clear();
9861126
this._cacheFS.clear();
9871127
this._unmockList = undefined;
9881128

@@ -1374,8 +1514,11 @@ export default class Runtime {
13741514
);
13751515
}
13761516

1377-
private _shouldMock(from: Config.Path, moduleName: string): boolean {
1378-
const explicitShouldMock = this._explicitShouldMock;
1517+
private _shouldMock(
1518+
from: Config.Path,
1519+
moduleName: string,
1520+
explicitShouldMock: Map<string, boolean>,
1521+
): boolean {
13791522
const moduleID = this._resolver.getModuleID(
13801523
this._virtualMocks,
13811524
from,
@@ -1543,6 +1686,24 @@ export default class Runtime {
15431686
this.setMock(from, moduleName, mockFactory, options);
15441687
return jestObject;
15451688
};
1689+
const mockModule: Jest['mockModule'] = (
1690+
moduleName,
1691+
mockFactory,
1692+
options,
1693+
) => {
1694+
if (mockFactory !== undefined) {
1695+
this.setModuleMock(from, moduleName, mockFactory, options);
1696+
return jestObject;
1697+
}
1698+
1699+
const moduleID = this._resolver.getModuleID(
1700+
this._virtualMocks,
1701+
from,
1702+
moduleName,
1703+
);
1704+
this._explicitShouldMockModule.set(moduleID, true);
1705+
return jestObject;
1706+
};
15461707
const clearAllMocks = () => {
15471708
this.clearAllMocks();
15481709
return jestObject;
@@ -1641,6 +1802,7 @@ export default class Runtime {
16411802
isMockFunction: this._moduleMocker.isMockFunction,
16421803
isolateModules,
16431804
mock,
1805+
mockModule,
16441806
requireActual: this.requireActual.bind(this, from),
16451807
requireMock: this.requireMock.bind(this, from),
16461808
resetAllMocks,

0 commit comments

Comments
 (0)