I want to talk about a practice that has more or less become the defacto for javascript unit testing: mocking module imports.
I have been wary of this from when it first came about with rewire and similar libraries. As time has gone on my opinion has only strengthened, and with jest actively embracing mocking modules, I feel like nobody has even bothered to make a counter argument.
So, this is my argument against mocking module imports.
What is a mocked import?
When you're writing code, you will at some point need to access some external resource. Fetching data over the network, reading a file from disk, etc.
When you're trying to test your code, you really don't want to be using these external resources. Imagine if your tests had to manage reading and writing files. You'd have to write the initial file state, run the test, tear down the file, hope there aren't any write errors or permission issues. Your tests would be slow, extremely brittle, difficult to run in a CI environment, impossible to run concurrently, and so on.
So we dont want to read or write real files during our tests. Ideally we want a fake fs
module we can mock and assert against.
The idea of mocking imports is that you can patch node's require
function, intercept certain calls, and potentially provide an entirely different module.
Once this idea was formed, it took off and this is what we have basically done ever since. It does the job, right? I can mock the fs module in my test, and then my code will get the mock when it tries to import it. I can spy on it, I can return a mocked response from fs.readFile
. So what's the problem?
Tightly coupled to node
Module mocking is literally a monkey patch over node's require function. It doesn't operate on documented features of node. It is, essentially, a hack. There is no guarantee that the node implementation will always be compatible with the way module mocking currently works. In fact I would highly suspect that node releases have caused module mocking libraries to fundamentally break in the past.
We now also have native esmodule support in most environments, including node. But this is not how esmodules are meant to be used, they weren't created with module mocking in mind.
Sometimes monkey patches and hacks are a necessary evil in life, but it shouldn't form the basis of every test we write... should it?
Tightly coupled to implementations
Perhaps this is subjective, but I strongly believe a low level action like reading/writing should be kept as far away as possible from high level code. In other words: in high level code, low level actions should be abstracted.
Say you're fetching an auth token and you want to store it. It's so easy to just import cookies from 'browser-cookies'
, call cookies.set
and you're done. I mean this is why javascript is so flexible right?
But should your auth function really know about cookies? What if you decide you'd prefer local or session storage? Or you even decide to just keep the token in memory. Abstraction is key to clean elegant code.
What does this have to do with mocking modules? It doesn't force tight coupling directly, but it does encourage this. Or perhaps it's more appropriate to say it encourages laziness.
Instead of asking "how do I separate my low level implementation detail from my application?", considering structure, maintainability, and coding practices; it's too easy to just go "ah well let's just mock the entire module import and move on".
Leaky Tests
When you mock a module, you're mocking the module for that entire test suite, potentially the entire test run, depending on your test runner.
How many times have you had a test fail because of another test? We're now adding even more global side effects into the mix.
What if every test requires a completely different response from your mocked module? What if you only want to mock part of the module? What if you want to completely undo a module mock for a single test? There are workarounds for these things, of course, but they are all workarounds and hacks, and quite often create more issues than they solve.
Side effects
For me, all of these points really boil down to a single fundamental problem: side effects - both intended and unintended.
Using the fs module directly will cause side effects in your code. Mocking the fs module will cause side effects in your tests. Mocking global objects like window.matchMedia
comes with the same issues as module mocking.
Solution
I believe the solution is one that is a fundamental part of almost all languages except javascript: Dependency Inversion.
I'm not even suggesting you use some high level ioc container framework or injection library here. Basic dependency inversion can be done without any complexity or overhead!
You want to read from a file? Provide fs
as a parameter. Want to check window.matchMedia
in your react component? Create a context provider for it - the context api is dependency inversion!
When you start inverting your dependencies, unit testing becomes so much easier! And for integration/e2e testing you can just leave dependencies in place.
There are libraries/frameworks out there to give you a full DI experience. I'd highly recommend at least trying one out. I have a bias, of course, for my own DI library but this is not a plugging article, I just want to bring attention to the fact that there is a very simple and easy solution to the issues caused by module mocking!
Top comments (0)