Belar

Mocking Node’s native ECMAScript modules with a custom loader


Landscape of mocking native ES modules in Node is complicated. Most tools are only now being developed, coming with limitations caveats, and yet to be solved issues. They also add to already busy lists of 3rd party dependencies.

However, there is a way to do it yourself. If for some reason you need to lightly customise loading of modules in your project, like mocking for purpose of testing, read on.

Module loader hooks

Loader hooks’ resolve is part of an experimental API and allows for modification of module (or its parent) URL’s resolution e.g. it can “redirect” it to a different module.

Loader is an ECMAScript module itself, and can be applied with —experimental-loader (earlier —loader) argument.

Mock-module loading

Below is a custom loader that will replace any module with its mock counterpart, if one is present in a specified __mocks__ directory.

// loader.js

import { basename, join, dirname } from "path";
import { existsSync } from "fs";
import { fileURLToPath, pathToFileURL } from "url";

const MOCKS_DIR_PATH = "./__mocks__";

const filePath = fileURLToPath(import.meta.url);
const fileDirname = dirname(filePath);

export function resolve(specifier, context, next) {
	const moduleName = basename(specifier);
	const moduleMockPath = join(fileDirname, MOCKS_DIR_PATH, moduleName);

	const hasMock = existsSync(moduleMockPath);
	if (hasMock) {
		const { href: url } = pathToFileURL(moduleMockPath);
		return {
			shortCircuit: true,
			url,
		};
	}

	return next(specifier, context);
}

Example run

index.js imports a.js module that has a mock defined at ./__mocks__/a.js (path relative to loader.js’s location). With the custom loader used, index.js will be supplied the mock, ./__mocks__/a.js, instead of the original module.

// a.js

console.log("Module called.");
// ./__mocks__/a.js

console.log("Module's mock called.");
// index.js

import "./a.js";

Default run, node ./index.js, will produce “Module called”. A run with the loader, node —experimental-loader ./loader.js ./index.js , will result in “Module’s mock called”.