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-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))
Expand Down
13 changes: 13 additions & 0 deletions docs/JestObjectAPI.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)`
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};
_isolatedMockRegistry: ?{[key: string]: any, __proto__: null};
_moduleMocker: ModuleMocker;
_isolatedModuleRegistry: ?ModuleRegistry;
_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._isolatedModuleRegistry = null;
this._moduleRegistry = Object.create(null);
this._needsCoverageMapped = new Set();
this._resolver = resolver;
Expand Down Expand Up @@ -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);
Expand All @@ -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
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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) {
Expand All @@ -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);

Expand Down Expand Up @@ -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);

Expand Down Expand Up @@ -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,
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,
|};