-
Notifications
You must be signed in to change notification settings - Fork 43
Feature: Mock modules (injection) #98
Comments
I've been thinking a lot about that one (and looked at our rewire usage). Generally mocking through the loader isn't a great use of the loader IMO because it's better done with dependency injection. That said, mocking tools today can also swap actual Note that one thing that arises from this feature if we choose to tackle it with multiple loaders is that we might need a way to change the loaders depending on what part of the same project we run (tests/app). |
@benjamingr I think this can be achieved with the So, I would translate: var proxyquire = require('proxyquire');
var foo = proxyquire('./foo', { 'path': pathStub });
foo.doSomething() To import proxyloader from 'proxyloader';
async function myTest() {
const foo = await import('./foo', {
loader: proxyloader({ 'path': pathStub })
});
foo.doSomething()
} |
That would change timings in this module compared to all across the module graph on subtle ways - i don’t think that would work unless every single module was also moved to an async IIFE |
this can be handled just fine by loader hooks, you shouldn't be setting up mocks by modifying your source anyway. |
Dynamic import doesn't accept a second options argument at the moment. There's also scenarios of folks mocking and unmocking in the same test file. import mock from "mock-require"
import requireInject from "require-inject"
mock("./real1.js", "./mock1.js")
import("./load.js")
.then((ns) => {
const exported = requireInject("./load.js", {
"./real2.js": "mock2"
})
console.log("mock-require:", JSON.stringify(ns))
// mock-require: {"real1":"mock1","real2":"real2"}
console.log("require-inject:", JSON.stringify(exported))
// mock-require: {"real1":"mock1","real2":"mock2"}
mock.stopAll()
return import("./load.js")
})
.then((ns) => {
console.log("mock-require:", JSON.stringify(ns))
// mock-require: {"real1":"real1","real2":"real2"}
}) |
I recently followed up some discussion around this topic in the webpack repo. Basically they made imports immutable in the last version, and it triggered some people that relied on those being mutable for stubbing. I did some thought experiment on how this could be done if there were loaders supported at runtime. And I arrived at something pretty close to what @mcollina proposes. import { mock } from 'mock-library';
async function test () {
const foo = await import(mock('./foo', {
path: await import('./pathStub')
}));
foo.doSomething();
} In here, the |
This can be done in a variety of ways, I have one possible way as an example in https://github.com/bmeck/node-apm-loader-example that uses loaders and has some special scoping mechanisms going on. The general idea is to intercept the imports like @Janpot describes. Unmocking is a bit harder since you cannot fully preserve live bindings with this approach. The loader is able to create its own mock bindings, but those bindings are unable to delegate to other modules. That means that the loader must manually sync the bindings if it wishes to appear like the original module. The timing of that synchronization is not a well defined thing and would only be possible to fully track if events were created on the exported binding changing or VM support for binding delegation. We could change the example to have synchronization events in order to preserve live bindings, but it would be a somewhat different workflow. In particular, instead of creating a "wrapper" module like https://github.com/bmeck/node-apm-loader-example/blob/master/overloads/fs.mjs is, it would instead completely transform the target module so that all exported variable assignments triggered synchronization. This would not work for builtins however unless the CJS form was also intercepted since builtins are using their own synchronization method as defined in nodejs/node#20403 . This feature is complex enough to get right that we should probably invest in making this easier to get synchronization events than it currently is. I am not sure we need to create a separate feature from loaders for the mocking feature specifically. |
There are 2 wrong ways of "dependency mocking":
They both are "wrong" as long as everything you can do - you can do after module is loaded. There are 2 right ways of "dependency mocking":
In both cases, have first to locate modules to be replaced, replace them, remove cache before them to allow re-require, require the module under test with dependencies rewired, and then restore the module system to a previous state. node-apm-loader-example is a good example of how to replace a single module, but cache-management moment is a bit missing. Without it - nothing would work. |
Use case 7.
The text was updated successfully, but these errors were encountered: