diff --git a/CHANGELOG.md b/CHANGELOG.md index 1531bcf03b26..6b67c1e28618 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ ### Features +- `[jest-runtime]` Add `jest.isolateModules` for scoped module initialization ([#6701](https://github.com/facebook/jest/pull/6701)) - `[jest-cli]` [**BREAKING**] Only set error process error codes when they are non-zero ([#7363](https://github.com/facebook/jest/pull/7363)) - `[jest-config]` [**BREAKING**] Deprecate `setupTestFrameworkScriptFile` in favor of new `setupFilesAfterEnv` ([#7119](https://github.com/facebook/jest/pull/7119)) - `[jest-worker]` [**BREAKING**] Add functionality to call a `setup` method in the worker before the first call and a `teardown` method when ending the farm ([#7014](https://github.com/facebook/jest/pull/7014)) diff --git a/docs/JestObjectAPI.md b/docs/JestObjectAPI.md index 7d42bb08235e..3873dc92328b 100644 --- a/docs/JestObjectAPI.md +++ b/docs/JestObjectAPI.md @@ -265,6 +265,19 @@ test('works too', () => { Returns the `jest` object for chaining. +### `jest.isolateModules(fn)` + +`jest.isolateModules(fn)` goes a step further than `jest.resetModules()` and creates a sandbox registry for the modules that are loaded inside the callback function. This is useful to isolate specific modules for every test so that local module state doesn't conflict between tests. + +```js +let myModule; +jest.isolateModules(() => { + myModule = require('myModule'); +}); + +const otherCopyOfMyModule = require('myModule'); +``` + ## Mock functions ### `jest.fn(implementation)` diff --git a/packages/jest-runtime/src/__tests__/runtime_require_module_or_mock.test.js b/packages/jest-runtime/src/__tests__/runtime_require_module_or_mock.test.js index 267236621968..f2d0127d6100 100644 --- a/packages/jest-runtime/src/__tests__/runtime_require_module_or_mock.test.js +++ b/packages/jest-runtime/src/__tests__/runtime_require_module_or_mock.test.js @@ -175,3 +175,110 @@ it('unmocks modules in config.unmockedModulePathPatterns for tests with automock const moduleData = nodeModule(); expect(moduleData.isUnmocked()).toBe(true); })); + +describe('resetModules', () => { + it('resets all the modules', () => + createRuntime(__filename, { + moduleNameMapper, + }).then(runtime => { + let exports = runtime.requireModuleOrMock( + runtime.__mockRootPath, + 'ModuleWithState', + ); + expect(exports.getState()).toBe(1); + exports.increment(); + expect(exports.getState()).toBe(2); + runtime.resetModules(); + exports = runtime.requireModuleOrMock( + runtime.__mockRootPath, + 'ModuleWithState', + ); + expect(exports.getState()).toBe(1); + })); +}); + +describe('isolateModules', () => { + it('resets all modules after the block', () => + createRuntime(__filename, { + moduleNameMapper, + }).then(runtime => { + let exports; + runtime.isolateModules(() => { + exports = runtime.requireModuleOrMock( + runtime.__mockRootPath, + 'ModuleWithState', + ); + expect(exports.getState()).toBe(1); + exports.increment(); + expect(exports.getState()).toBe(2); + }); + + exports = runtime.requireModuleOrMock( + runtime.__mockRootPath, + 'ModuleWithState', + ); + expect(exports.getState()).toBe(1); + })); + + it('cannot nest isolateModules blocks', () => + createRuntime(__filename, { + moduleNameMapper, + }).then(runtime => { + expect(() => { + runtime.isolateModules(() => { + runtime.isolateModules(() => {}); + }); + }).toThrowError( + 'isolateModules cannot be nested inside another isolateModules.', + ); + })); + + it('can call resetModules within a isolateModules block', () => + createRuntime(__filename, { + moduleNameMapper, + }).then(runtime => { + let exports; + runtime.isolateModules(() => { + exports = runtime.requireModuleOrMock( + runtime.__mockRootPath, + 'ModuleWithState', + ); + expect(exports.getState()).toBe(1); + + exports.increment(); + runtime.resetModules(); + + exports = runtime.requireModuleOrMock( + runtime.__mockRootPath, + 'ModuleWithState', + ); + expect(exports.getState()).toBe(1); + }); + + exports = runtime.requireModuleOrMock( + runtime.__mockRootPath, + 'ModuleWithState', + ); + expect(exports.getState()).toBe(1); + })); + + describe('can use isolateModules from a beforeEach block', () => { + let exports; + beforeEach(() => { + jest.isolateModules(() => { + exports = require('./test_root/ModuleWithState'); + }); + }); + + it('can use the required module from beforeEach and re-require it', () => { + expect(exports.getState()).toBe(1); + exports.increment(); + expect(exports.getState()).toBe(2); + + exports = require('./test_root/ModuleWithState'); + expect(exports.getState()).toBe(1); + exports.increment(); + expect(exports.getState()).toBe(2); + }); + }); +}); diff --git a/packages/jest-runtime/src/__tests__/test_root/ModuleWithState.js b/packages/jest-runtime/src/__tests__/test_root/ModuleWithState.js new file mode 100644 index 000000000000..85359a4f70c5 --- /dev/null +++ b/packages/jest-runtime/src/__tests__/test_root/ModuleWithState.js @@ -0,0 +1,15 @@ +/** + * Copyright (c) 2014-present, Facebook, Inc. All rights reserved. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +let state = 1; + +export const increment = () => { + state += 1; +}; + +export const getState = () => state; diff --git a/packages/jest-runtime/src/__tests__/test_root/root.js b/packages/jest-runtime/src/__tests__/test_root/root.js index 1e0b5acd4974..02add6e11338 100644 --- a/packages/jest-runtime/src/__tests__/test_root/root.js +++ b/packages/jest-runtime/src/__tests__/test_root/root.js @@ -11,6 +11,7 @@ require('ExclusivelyManualMock'); require('ManuallyMocked'); require('ModuleWithSideEffects'); +require('ModuleWithState'); require('RegularModule'); // We only care about the static analysis, not about the runtime. diff --git a/packages/jest-runtime/src/index.js b/packages/jest-runtime/src/index.js index 84601fccd9c5..d29cf5fc590e 100644 --- a/packages/jest-runtime/src/index.js +++ b/packages/jest-runtime/src/index.js @@ -97,7 +97,9 @@ class Runtime { _mockFactories: {[key: string]: () => any, __proto__: null}; _mockMetaDataCache: {[key: string]: MockFunctionMetadata, __proto__: null}; _mockRegistry: {[key: string]: any, __proto__: null}; + _isolatedMockRegistry: ?{[key: string]: any, __proto__: null}; _moduleMocker: ModuleMocker; + _isolatedModuleRegistry: ?ModuleRegistry; _moduleRegistry: ModuleRegistry; _needsCoverageMapped: Set; _resolver: Resolver; @@ -132,6 +134,7 @@ class Runtime { this._mockFactories = Object.create(null); this._mockRegistry = Object.create(null); this._moduleMocker = this._environment.moduleMocker; + this._isolatedModuleRegistry = null; this._moduleRegistry = Object.create(null); this._needsCoverageMapped = new Set(); this._resolver = resolver; @@ -291,11 +294,6 @@ class Runtime { ); let modulePath; - const moduleRegistry = - !options || !options.isInternalModule - ? this._moduleRegistry - : this._internalModuleRegistry; - // Some old tests rely on this mocking behavior. Ideally we'll change this // to be more explicit. const moduleResource = moduleName && this._resolver.getModule(moduleName); @@ -319,6 +317,18 @@ class Runtime { modulePath = this._resolveModule(from, moduleName); } + let moduleRegistry; + + if (!options || !options.isInternalModule) { + if (this._moduleRegistry[modulePath] || !this._isolatedModuleRegistry) { + moduleRegistry = this._moduleRegistry; + } else { + moduleRegistry = this._isolatedModuleRegistry; + } + } else { + moduleRegistry = this._internalModuleRegistry; + } + if (!moduleRegistry[modulePath]) { // We must register the pre-allocated module object first so that any // circular dependencies that may arise while evaluating the module can @@ -360,12 +370,16 @@ class Runtime { moduleName, ); - if (this._mockRegistry[moduleID]) { + if (this._isolatedMockRegistry && this._isolatedMockRegistry[moduleID]) { + return this._isolatedMockRegistry[moduleID]; + } else if (this._mockRegistry[moduleID]) { return this._mockRegistry[moduleID]; } + const mockRegistry = this._isolatedMockRegistry || this._mockRegistry; + if (moduleID in this._mockFactories) { - return (this._mockRegistry[moduleID] = this._mockFactories[moduleID]()); + return (mockRegistry[moduleID] = this._mockFactories[moduleID]()); } let manualMock = this._resolver.getMockModule(from, moduleName); @@ -409,15 +423,15 @@ class Runtime { // Only include the fromPath if a moduleName is given. Else treat as root. const fromPath = moduleName ? from : null; - this._execModule(localModule, undefined, this._mockRegistry, fromPath); - this._mockRegistry[moduleID] = localModule.exports; + this._execModule(localModule, undefined, mockRegistry, fromPath); + mockRegistry[moduleID] = localModule.exports; localModule.loaded = true; } else { // Look for a real module to generate an automock from - this._mockRegistry[moduleID] = this._generateMock(from, moduleName); + mockRegistry[moduleID] = this._generateMock(from, moduleName); } - return this._mockRegistry[moduleID]; + return mockRegistry[moduleID]; } requireModuleOrMock(from: Path, moduleName: string) { @@ -443,7 +457,22 @@ class Runtime { } } + isolateModules(fn: () => void) { + if (this._isolatedModuleRegistry || this._isolatedMockRegistry) { + throw new Error( + 'isolateModules cannot be nested inside another isolateModules.', + ); + } + this._isolatedModuleRegistry = Object.create(null); + this._isolatedMockRegistry = Object.create(null); + fn(); + this._isolatedModuleRegistry = null; + this._isolatedMockRegistry = null; + } + resetModules() { + this._isolatedModuleRegistry = null; + this._isolatedMockRegistry = null; this._mockRegistry = Object.create(null); this._moduleRegistry = Object.create(null); @@ -902,6 +931,10 @@ class Runtime { this.resetModules(); return jestObject; }; + const isolateModules = (fn: () => void) => { + this.isolateModules(fn); + return jestObject; + }; const fn = this._moduleMocker.fn.bind(this._moduleMocker); const spyOn = this._moduleMocker.spyOn.bind(this._moduleMocker); @@ -938,6 +971,7 @@ class Runtime { this._generateMock(from, moduleName), getTimerCount: () => this._environment.fakeTimers.getTimerCount(), isMockFunction: this._moduleMocker.isMockFunction, + isolateModules, mock, requireActual: localRequire.requireActual, requireMock: localRequire.requireMock, diff --git a/types/Jest.js b/types/Jest.js index 889c45382a95..1afa267e3942 100644 --- a/types/Jest.js +++ b/types/Jest.js @@ -50,4 +50,5 @@ export type Jest = {| unmock(moduleName: string): Jest, useFakeTimers(): Jest, useRealTimers(): Jest, + isolateModules(fn: () => void): Jest, |};