Skip to content

Commit

Permalink
Add jest.isolateModules for scoped module initialization (#6701)
Browse files Browse the repository at this point in the history
* First iteration of withResetModules

* Make withResetModules "not async"

* Reimplement withResetModules without the stack

* Rename withResetModules to isolateModules

* Add changelog entry

* Fix eslint

* Add docs for isolateModules

* Fix typo, remove provideModules and extra async stuff

* Update Configuration.md

* Rename implementation to isolated* and change docs to JestObjectAPI
  • Loading branch information
rogeliog authored and thymikee committed Dec 18, 2018
1 parent 1818d84 commit 873c641
Show file tree
Hide file tree
Showing 7 changed files with 183 additions and 11 deletions.
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,
|};

0 comments on commit 873c641

Please sign in to comment.