/ #javascript #node #node:test 

Clear Module Cache/Force Import in Node.js with CommonJS and with ES Modules (ESM)

Node.js supports two primary module systems: CommonJS and ES Modules (ESM). Both systems cache imported modules, ensuring that a module’s code runs only once, even if it’s imported multiple times.

When writing tests it can be useful to bypass caching for example if the code being tested is executed at the module scope (outside of exported functions).

While most test frameworks handle this for you eg. with jest.mock/vi.mock, when using a more barebones testing framework like node:test (built-in Node.js test runner) or in rare exceptions in application code.

Table of Contents

CommonJS: clearing module cache

In the CommonJS module system, which uses require, the cache is available on the require.cache object. This object stores the cached modules and can be manipulated. For instance, if you want to force a module to re-run its code upon re-import, you can simply delete the module from require.cache like this:

delete require.cache[require.resolve('./path/to/module')];

This clears the cache for the specified module, allowing it to be reloaded and re-executed upon the next require call.

However, things work differently in the ES Modules (ESM) system.

ESM: import with Cache-Busting query parameter

ESM doesn’t expose a module cache that you can manipulate directly. As a result, you can’t delete a module from the cache as you can in CommonJS. Instead, in ESM we can force (re-)evaluation of a module by using dynamic imports and a query parameter to achieve “cache-busting”.

The most common method to force an import to re-run in ESM is by using a dynamic import() statement combined with a query string that changes with each import. For example:

// this will run the module again
const mod = await import(`./path/to/file.js?t=${Date.now()}`);

Note that the query string doesn’t need to be dynamic, so we could’ve used ./path/to/file.js?bust=my-key as well.

By appending a query string with a unique value, such as the current epoch, you effectively bypass the cache. This tricks Node.js into treating the import as a request for a new module, thus re-executing the module’s code.

Practical Example: Node.js test runner, testing an ES module with Cache-Busting Imports

The “Cache-busting dynamic import” technique is particularly useful in testing scenarios. Let’s say you have an index.js file containing the following code:

console.log(process.env.MY_VAR);

You want to test this file with different values of process.env.MY_VAR without restarting your test suite. You can achieve this by using cache-busting imports in your tests, as shown below (in an index.test.js file):

import { test, mock } from 'node:test';
import assert from 'node:assert/strict';

test('log vars - undefined', async () => {
  const log = mock.method(console, 'log');
  log.mock.mockImplementation(() => {});
  await import('./index.js?var-undefined');
  assert.equal(log.mock.callCount(), 1);
});

test('log vars - set', async () => {
  const log = mock.method(console, 'log');
  process.env.MY_VAR = 'TEST_123';
  log.mock.mockImplementation(() => {});
  await import('./index.js?var-set');
  assert.equal(log.mock.callCount(), 1);
  assert.deepEqual(log.mock.calls[0].arguments, ['TEST_123']);
});

In this example, the query string passed to import() ensures that each import is treated as a separate module execution, even though the underlying file is the same. This allows you to test different scenarios within the same test suite run.

We can validate that this test suite runs as expected using node --test (Node.js native test runner module):

node --test
✔ log vars - undefined (4.358584ms)
✔ log vars - set (0.8485ms)
ℹ tests 2
ℹ suites 0
ℹ pass 2
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 78.649334

For a full example of this approach in action, check out this GitHub repository.

Conclusion

We’ve now seen how to force re-imports in CommonJS by clearing the module cache and in ESM by using dynamic imports with a cache-busting parameter. These techniques can be used to ensure module evaluation behaves as expected under different conditions.

Author

Hugo Di Francesco

Co-author of "Professional JavaScript", "Front-End Development Projects with Vue.js" with Packt, "The Jest Handbook" (self-published). Hugo runs the Code with Hugo website helping over 100,000 developers every month and holds an MEng in Mathematical Computation from University College London (UCL). He has used JavaScript extensively to create scalable and performant platforms at companies such as Canon, Elsevier and (currently) Eurostar.

Interested in Alpine.js?

Power up your debugging with the Alpine.js Devtools Extension for Chrome and Firefox. Trusted by over 15,000 developers (rated 4.5 ⭐️).