Skip to content

Instantly share code, notes, and snippets.

@guybedford
Last active October 23, 2021 23:22
Show Gist options
  • Save guybedford/e6687e77d60739f27d1b5449a2872bf4 to your computer and use it in GitHub Desktop.
Save guybedford/e6687e77d60739f27d1b5449a2872bf4 to your computer and use it in GitHub Desktop.
Node.js mocking example
const mocks = Object.create(null);
global.mock = function (_mocks) {
Object.assign(mocks, _mocks);
};
export async function resolve (specifier, context, parentResolve) {
if (mocks[specifier]) {
return { url: 'mock:' + specifier };
}
return parentResolve(specifier, context);
}
export async function getFormat(url, context, parentGetFormat) {
if (url.startsWith('mock:')) {
return { format: 'dynamic' };
}
return parentGetFormat(url, context, parentGetFormat);
}
export async function dynamicInstantiate (url) {
const obj = mocks[url.slice(5)];
const exports = Object.keys(obj);
return {
exports,
execute (ns) {
for (const key of exports)
ns[key].set(obj[key]);
}
}
}
node --loader ./mock-loader.mjs test.mjs
# Alternatively, locate at node_modules/mock-loader/loader.mjs with a package.json "exports": "./loader.mjs" set
# Then use something like:
# NODE_OPTIONS="--loader mock-loader" node test.mjs
mock({
express: {
custom: 'express'
}
});
import('express').then(x => console.log(x));
@guybedford
Copy link
Author

Will the loader and the main process share two different set of globals? How could the same API keep working if so? The important part is the the fact that any closures that are passed through mock() are retained.

They can't! Rather, the main thread mock call would use a unique identifier that can serialize to the loader thread, that then coordinates that the binding run through the mock call is used for the main thread module instance. That is, the binding is still created, setup and shared on the main thread, but the loader on the other thread has to coordinate that through a serialization interface over having access to the binding explicitly.

My biggest concern is that without mocking capabilities, the only viable approach to testing esm is to use heavy transpilation similar to what jest does, which is not really esm anyway. (The distance of what jest executes vs what node executes is significant).

If you consider the mocking example above enough capability, then I can guarantee that the capability will remain possible despite any future API changes. The API just won't necessarily be stable. It should be possible for a userland mocking API to be created that provides a stable interface to users working over all the unstable versions. Happy to discuss collaboration further there too if you like.

The distance of what jest executes vs what node executes is significant

Can you clarify your point here further?

@mcollina
Copy link

Can you clarify your point here further?

Jest mocks rely on transpiling instead of using any proper real-time esm mechanism. Essentially they are fundamentally incompatibile with Node.js esm implementation - they sidestep it completey as they are fully transpiling everything.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment