Jest does some interesting things to Node’s default require behavior. In an attempt to encourage test independence and concurrent test execution, Jest resets the module cache after every test.

You may remember one of my previous articles about “bending Jest to our will” and caching instances of modules across multiple tests. While that solution works for single modules on a case-by-case basis, sometimes that’s not quite enough. Sometimes we just want to completely restore Node’s original require behavior across the board.

After sleuthing through support tickets, blog posts, and “official statements” from Jest core developers, this seems to be entirely unsupported and largely impossible.

However, with some highly motivated hacking I’ve managed to find a way.

Our Goal

If you’re unfamiliar with how require works under the hood, here’s a quick rundown. The first time a module is required, its contents are executed and the resulting exported data is cached. Any subsequent require calls of the same module return a reference to that cached data.

That’s all there is to it.

Jest overrides this behavior and maintains its own “module registry” which is blown away after every test. If one test requires a module, the module’s contents are executed and cached. If that same test requires the same module, the cached result will be returned, as we’d expect. However, other tests don’t have access to our first test’s module registry. If another test tries to require that same module, it’ll have to execute the module’s contents and store the result in its own private module registry.

Our goal is to find a way to reverse Jest’s monkey-patching of Node’s default require behavior and restore it’s original behavior.

This change, or reversal of a change, will have some unavoidable consequences. Our Jest test suite won’t be able to support concurrent test processes. This means that all our tests will have to run “in band”(--runInBand). More interestingly, Jest’s “watch mode” will no longer work, as it uses multiple processes to run tests and maintain a responsive command line interface.

Accepting these limitations and acknowledging that this is likely a very bad idea, let’s press on.

Dependency Hacking

After several long code reading and debugging sessions, I realized that the heart of the problem resides in Jest’s jest-runtime module. Specifically, the requireModuleOrMock function, which is responsible for Jest’s out-of-the-box require behavior. Jest internally calls this method whenever a module is required by a test or by any code under test.

Short circuiting this method with a quick and dirty require causes the require statements throughout our test suites and causes our code under test to behave exactly as we’d expect:


requireModuleOrMock(from: Path, moduleName: string) {
+ return require(this._resolveModule(from, moduleName));
  try {
    if (this._shouldMock(from, moduleName)) {
      return this.requireMock(from, moduleName);
    } else {
      return this.requireModule(from, moduleName);
    }
  } catch (e) {
    if (e.code === 'MODULE_NOT_FOUND') {
      const appendedMessage = findSiblingsWithFileExtension(
        this._config.moduleFileExtensions,
        from,
        moduleName,
      );

      if (appendedMessage) {
        e.message += appendedMessage;
      }
    }
    throw e;
  }
}

Whenever Jest reaches for a module, we relieve it of the decision to use a cached module from it’s internally maintained moduleRegistry, and instead have it always return the result of requiring the module through Node’s standard mechanisms.

Patching Jest

Our fix works, but in an ideal world we wouldn’t have to fork jest-runtime just to make our change. Thankfully, the requireModuleOrMock function isn’t hidden within a closure or made inaccessible through other means. This means we’re free to monkey-patch it ourselves!

Let’s start by creating a test/globalSetup.js file in our project to hold our patch. Once created, we’ll add the following lines:


const jestRuntime = require('jest-runtime');

jestRuntime.prototype.requireModuleOrMock = function(from, moduleName) {
    return require(this._resolveModule(from, moduleName));
};

We’ll tell our Jest setup to use this config file by listing it in our jest.config.js file:


module.exports = {
    globalSetup: './test/globalSetup.js',
    ...
};

And that’s all there is to it! Jest will now execute our globalSetup.js file once, before all of our test suites, and restore the original behavior of require.

Being the future-minded developers that we are, it’s probably wise to document this small and easily overlooked bit of black magic:


/*
 * This requireModuleOrMock override is _very experimental_. It affects
 * how Jest works at a very low level and most likely breaks Jest-style
 * module mocks.
 *
 * The upside is that it lets us evaluate heavy modules once, rather
 * that once per test.
 */

jestRuntime.prototype.requireModuleOrMock = function(from, moduleName) {
    return require(this._resolveModule(from, moduleName));
};

If you find yourself with no other choice but to perform this incantation on your test suite, I wish you luck. You’re most likely going to need it.