Jest module isolation: when to rely on per-file registry vs resetModules and isolateModules
Jest isolates per test file by default, but within a file the module registry is shared. Use isolateModules for targeted freshness and resetModules only when needed, with checks for globals and ordering.
14 Mar 2026, 09:08 UTC

Tests pass alone but fail together
The classic signal is a suite that is green in isolation and red when run with the rest of the repo. In Jest that usually means a module-level singleton, cache or mutated export is leaking between tests. The fix is not always to reset everything; it is to match the isolation tool to the actual trust boundary.
Requirements for deterministic tests
Determinism requires that a test's observable state is set up by its own arrange phase and not by the execution order of previous tests. For module state that means top-level module initialization runs once per test context, mutations to exported objects do not persist unless intended, and global process state, timers and handles are cleaned up.
Jest provides some of this out of the box and some only on explicit request.
Trust and data boundaries Jest provides
Per-file registry and worker
By default Jest runs each test file in its own module registry and, in parallel mode, in its own worker process. That is the baseline trust boundary: top-level state in a.test.js cannot leak into b.test.js via require. The boundary is per file, not per test.
Within a file the registry is shared
Inside a single test file the module registry is reused across test and describe blocks. If a module exports a mutable singleton, the first require populates the registry and subsequent requires return the same instance. Mutations persist.
jest.resetModules() clears the internal registry. A subsequent require will re-execute the module factory and return a fresh instance. It does not revert mutations to globals, process.env, or references already held in variables.
jest.isolateModules(fn) creates a temporary sandboxed registry for the duration of fn. Only modules required inside fn are fresh. This is the smallest suitable tool when only a few tests need a fresh copy.
Smallest suitable design
Start with the default per-file isolation and avoid blanket resets. Use targeted isolation only where statefulness is proven.
Example module with module-level state:
// counter.js
let count = 0;
module.exports = {
get: () => count,
inc: () => ++count,
reset: () => { count = 0; }
};Naive test file shows shared state:
// counter.test.js
const counter = require('./counter');
test('first inc', () => {
counter.inc();
expect(counter.get()).toBe(1);
});
test('second sees first', () => {
// With shared registry this will be 1, not 0
expect(counter.get()).toBe(0);
});If a fresh instance is required for a specific test, use isolateModules and require inside the callback:
test('fresh instance', () => {
jest.isolateModules(() => {
const fresh = require('./counter');
fresh.inc();
expect(fresh.get()).toBe(1);
});
});When many tests need a clean module, a beforeEach reset is more explicit:
beforeEach(() => {
jest.resetModules();
});
test('clean', () => {
const counter = require('./counter');
expect(counter.get()).toBe(0);
});Note: jest.mock() factories are hoisted per file. After resetModules you must re-register mocks or require the module again inside the test. Combining manual mocks with resetModules is a common source of confusing failures.
Operational checks
- Run the suite with
npx jest --runInBandfrom the repo root. This forces serial execution in one process and surfaces ordering dependencies that parallel workers can hide. Run as a developer with read access to the repo; no elevated permissions required. Risk: slower execution. - Run with
npx jest --detectOpenHandlesto find leaked timers or handles that module isolation cannot clean up. - Check the installed Jest version's documentation or changelog for
resetModulesandisolateModulesbehavior, especially if using ESM. Behavior around ESM mocking and isolation differs from CommonJS and has changed across versions.
Failure modes
Tests passing individually but failing together indicate shared singleton state within a file. Tests passing together but failing with --runInBand indicate assumptions about worker order or global state.
Both point to a misunderstood isolation boundary. Resetting modules does not undo mutations to process.env, global objects, or already-captured references. Those require explicit teardown in afterEach.
When to change the design
Change the design when the cost of isolation outweighs the benefit.
- Expensive read-only fixtures. Prefer a single setup module or shared factory over
resetModulesfor every test. The per-file boundary is sufficient if fixtures are immutable. - Process-global state. Module isolation does not protect
process.env, global singletons, or native modules. Use explicit save/restore or a dedicated test helper instead of relying on Jest registry resets. - Many tests need fresh state. If most tests require a clean module, consider refactoring the module to be stateless or to accept state via parameters rather than relying on
resetModulesin every test.
Limitations and practical verification
Limitations: resetModules only affects modules required after the reset. Existing references remain bound to the old instance. Globals and environment are untouched.
Verification steps to apply in your repo:
- Write a module with a module-level counter, mutate it in one test and assert persistence in a second test. Repeat with
jest.resetModules()inbeforeEachto confirm the registry is cleared. - Run the same suite with and without
--runInBandand compare results to surface hidden ordering dependencies. - Review the Jest version in use for ESM vs CommonJS isolation semantics before adopting
isolateModulesbroadly.
These checks confirm whether the chosen isolation level matches the actual data boundaries in your code.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.