Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add jest.isolateModules for scoped module initialization #6701

Merged
merged 14 commits into from
Dec 18, 2018
Merged
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

### Features

- `[jest-runtime]` Add `jest.isolateModules` for scoped module initialization ([#6701](https://github.com/facebook/jest/pull/6701))
- `[jest-config]` Add shorthand for watch plugins and runners ([#7213](https://github.com/facebook/jest/pull/7213))
- `[jest-config]` [**BREAKING**] Deprecate `setupTestFrameworkScriptFile` in favor of new `setupFilesAfterEnv` ([#7119](https://github.com/facebook/jest/pull/7119))
- `[jest-jasmine2/jest-circus/jest-cli]` Add test.todo ([#6996](https://github.com/facebook/jest/pull/6996))
Expand Down
15 changes: 15 additions & 0 deletions docs/Configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -551,6 +551,21 @@ Default: `false`

By default, each test file gets its own independent module registry. Enabling `resetModules` goes a step further and resets the module registry before running each individual test. This is useful to isolate modules for every test so that local module state doesn't conflict between tests. This can be done programmatically using [`jest.resetModules()`](#jest-resetmodules).

### `isolateModules` [boolean]

Default: `false`

`isolateModules` goes a step further than `resetModules` and creates a sandbox registry for the modules that are loaded inside the callback function. This is useful to isolate modules for every test so that local module state doesn't conflict between tests. This can be done programmatically using [`jest.isolateModules()`](#jest-isolatemodules).

```js
let myModule;
jest.isolateModules(() => {
myModule = require('myModule');
});
Copy link
Collaborator

@thymikee thymikee Oct 28, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should move the example to https://jestjs.io/docs/en/jest-object (and fix the link to jest.isolateModules() above)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point, I think it should only live in jest-object. This was my bad.


const otherCopyOfMyModule = require('myModule');
```

### `resolver` [string]

Default: `undefined`
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
});
});
});
15 changes: 15 additions & 0 deletions packages/jest-runtime/src/__tests__/test_root/ModuleWithState.js
Original file line number Diff line number Diff line change
@@ -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;
1 change: 1 addition & 0 deletions packages/jest-runtime/src/__tests__/test_root/root.js
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
56 changes: 45 additions & 11 deletions packages/jest-runtime/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,9 @@ class Runtime {
_mockFactories: {[key: string]: () => any, __proto__: null};
_mockMetaDataCache: {[key: string]: MockFunctionMetadata, __proto__: null};
_mockRegistry: {[key: string]: any, __proto__: null};
_sandboxMockRegistry: ?{[key: string]: any, __proto__: null};
_moduleMocker: ModuleMocker;
_sandboxModuleRegistry: ?ModuleRegistry;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think _isolatedModuleRegistry would play slightly better with the name isolateModules but I'm good with both names 👍.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍 I like it way better!

_moduleRegistry: ModuleRegistry;
_needsCoverageMapped: Set<string>;
_resolver: Resolver;
Expand Down Expand Up @@ -132,6 +134,7 @@ class Runtime {
this._mockFactories = Object.create(null);
this._mockRegistry = Object.create(null);
this._moduleMocker = this._environment.moduleMocker;
this._sandboxModuleRegistry = null;
this._moduleRegistry = Object.create(null);
this._needsCoverageMapped = new Set();
this._resolver = resolver;
Expand Down Expand Up @@ -289,11 +292,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);
Expand All @@ -317,6 +315,18 @@ class Runtime {
modulePath = this._resolveModule(from, moduleName);
}

let moduleRegistry;

if (!options || !options.isInternalModule) {
if (this._moduleRegistry[modulePath] || !this._sandboxModuleRegistry) {
moduleRegistry = this._moduleRegistry;
} else {
moduleRegistry = this._sandboxModuleRegistry;
}
} 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
Expand Down Expand Up @@ -358,12 +368,16 @@ class Runtime {
moduleName,
);

if (this._mockRegistry[moduleID]) {
if (this._sandboxMockRegistry && this._sandboxMockRegistry[moduleID]) {
return this._sandboxMockRegistry[moduleID];
} else if (this._mockRegistry[moduleID]) {
return this._mockRegistry[moduleID];
}

const mockRegistry = this._sandboxMockRegistry || 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);
Expand Down Expand Up @@ -407,15 +421,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) {
Expand All @@ -441,7 +455,22 @@ class Runtime {
}
}

isolateModules(fn: () => void) {
if (this._sandboxModuleRegistry || this._sandboxMockRegistry) {
throw new Error(
'isolateModules cannot be nested inside another isolateModules.',
);
}
this._sandboxModuleRegistry = Object.create(null);
this._sandboxMockRegistry = Object.create(null);
fn();
this._sandboxModuleRegistry = null;
this._sandboxMockRegistry = null;
}

resetModules() {
this._sandboxModuleRegistry = null;
this._sandboxMockRegistry = null;
this._mockRegistry = Object.create(null);
this._moduleRegistry = Object.create(null);

Expand Down Expand Up @@ -900,6 +929,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);

Expand Down Expand Up @@ -936,6 +969,7 @@ class Runtime {
this._generateMock(from, moduleName),
getTimerCount: () => this._environment.fakeTimers.getTimerCount(),
isMockFunction: this._moduleMocker.isMockFunction,
isolateModules,
mock,
requireActual: localRequire.requireActual,
requireMock: localRequire.requireMock,
Expand Down
1 change: 1 addition & 0 deletions types/Jest.js
Original file line number Diff line number Diff line change
Expand Up @@ -50,4 +50,5 @@ export type Jest = {|
unmock(moduleName: string): Jest,
useFakeTimers(): Jest,
useRealTimers(): Jest,
isolateModules(fn: () => void): Jest,
|};