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

Great to hear, please feel free to use! No attribution is necessary.

Note there are currently plans to separate loaders into their own thread. If that PR lands the technique here would have to adapt to that, effectively with some extra instrumentation on both ends. The same overall API can be supported fine with the new primitives though, and I can be sure to track that remains possible.

The other thing that might affect this in future is any changes to the dynamicInstantiate hook which is also considered unstable currently. Anything that replaces it would still permit the same functionality as well.

Personally I would like us to separate modules stability from loader stability - loaders are a much more advanced feature that do still need a little time to mature (especially without a lot of active development, I have had to draw a line on this myself without funding unfortunately as mentioned). I hope we could consider stable modules without stable loaders though, and that is an important discussion to have.

In terms of supporting API changes, in theory even as loader APIs mature it should still be possible to write loaders that work in older versions of Node.js using some ugly techniques, but there are possibilities there too.

@mcollina
Copy link

Note there are currently plans to separate loaders into their own thread. If that PR lands the technique here would have to adapt to that, effectively with some extra instrumentation on both ends. The same overall API can be supported fine with the new primitives though, and I can be sure to track that remains possible.

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.

Personally I would like us to separate modules stability from loader stability - loaders are a much more advanced feature that do still need a little time to mature (especially without a lot of active development, I have had to draw a line on this myself without funding unfortunately as mentioned). I hope we could consider stable modules without stable loaders though, and that is an important discussion to have.

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).

@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