When you’re unit testing, there is no way around it: from time to time you’ll need mocking. Modern testing libraries have support for mocking modules. This means injecting mocks into the module system. Both Jest and Vitest have support for this.
When you look at the examples in the documentation, it seems simple enough. But when you try to do it in a production code base, it becomes a big frustrating mess:
It would either work, or not work at all. It just felt completely random at times.
I have no clue what is happening behind the scenes when I run my tests.
It just feels so extremely magical.
Those are quotes from a thread I came across. Luckily, it doesn’t have to be this way.
Mocking modules is a TDD anti-pattern
Before we get into dependency Injection (DI), let’s see how mocking modules causes so much pain.
Since we hook into the module system, tests that share modules can no longer run in isolation, they will affect each other: if a module is mocked in one test, it will have to be mocked in other tests. And if you’re not careful, mocks from one test will creep into other tests. This is a TDD anti-pattern:
you should always start a unit test from a known and pre-configured state
A mock from one test creeping into another test is not “a known and pre-configured state”.
Modern testing libraries like Jest and Vitest do a good job of avoiding these problems because they:
- Keep the module systems of test files separated
- Always run tests from one file in the order that they are defined
- Suggest clearing all mocks after each test with an
But you still have to be careful what you’re doing:
- Want to mock a module in one test, but not in the other? Too bad, you’ll have to put the tests in different files.
- Forgot to clear mocks in a test? Pain.
it.concurrent()to run tests in parallel? Pain!
And on top of that: you need to know the library-specific API’s to inject your mocks into the module system.
Maybe this doesn’t seem too bad, but things can get quite subtle: you run your tests locally, everything seems fine, so you push the code. Your CI pipeline runs the tests, everything green, great! Life is good.
The next day you push some completely unrelated code and your test from the day before suddenly fails, WTF is going on?!
This is what we call “flaky” tests. They’re pretty much bugs in your tests, the worst kinds of bugs: those you can’t reproduce predictably because they only happen sometimes. That’s what happens when your tests do not start from a “known and pre-configured state”.
Plain JS Dependency Injection
What if I told you all of this pain can be avoided by applying just one technique, no new syntax or tool, just plain JS. Introducing: Dependency Injection (DI).
Wikipedia describes the goal of DI like this:
dependency injection aims to separate the concerns of constructing objects and using them, leading to loosely coupled programs.
It says “constructing objects” because this technique has its origin in object-oriented programming. You don’t need objects (or classes for that matter) to make use of it, we’ll be constructing a function.
This is the function we will be testing:
And a Jest test that mocks the Axios module:
We have to mock the Axios module here because there is no other way to make
getProducts() call a different Axios instance. The current implementation is tightly coupled to the default Axios instance.
We can fix that with DI, let’s see what the code looks like:
In the tests we can now use the factory function
createGetProducts() to create our own version of getProducts with a mocked Axios instance. No need to get fancy, the mock is also plain JS:
By using DI instead of module mocking:
- Your tests can run in isolation
- You do not need to know any framework-specific syntax / module magic to inject a mock
- Your code is more loosely coupled
Want to learn more? Get the FREE TDD training exercise!
TDD is not so much about the code you end up with, but about the way you got there. The best way to learn TDD, is to pair with an experienced engineer.
I can’t pair with every one of you, but there is something I can do:
Want to see how an experienced engineer uses TDD to build a web frontend and then apply it yourself in the same codebase?
Get the FREE TDD training exercise below.
We’ll go further than the trivial examples you find all over the internet, but without the stress and hassle of a real project. That way you learn things you can actually use in your day job.
We will be implementing a registration flow where users can sign up and confirm their account with a verification code.
- the exercise itself
- the solution
- a commit for each step of the way
- 1.5 hour of video:
- going through the entire exercise from start to finish
- with explanation of what’s happening and why
No spam, no sharing your data with third parties.