Mocking with Jest:Taking Advantage of the Module System
Published on Last modified on Parts of this series:
- Spying on Functions and Changing Implementation
- Taking Advantage of the Module System
Jest has lots of mocking features. For a long time I’ve been using only a small subset of them, but with experience I was able to gain a deeper understanding of these features. Now I want to share that knowledge with you because it has been incredibly useful to me.
This can be an intimidating area for beginners, especially because at the time of this writing the Jest documentation on this subject is a bit spotty. Add to that the fact that the term “mock” is ambiguous; it can refer to functions, modules, servers etc. I would like to help you get familiar not only with mocking features in Jest, but these testing concepts in general.
While this blog posts reads fine on its own, some of the references are from Mocking with Jest: Spying on Functions and Changing their Implementation, so I suggest starting there.
Whether we’re testing server or browser code, both of these are using a module system. We can take advantage of this by mocking certain dependencies during testing.
We often don’t want some of our modules to do what they normally do. For example, we don’t want to make an actual API request, instead we want to mock that implementation in a way that will make our code work without unwanted functionality.
Let’s say that the head of the Ministry of Silly Walks wanted to create a method for plotting their walking pattern as an array of steps using left and right legs:
// monty-python.js
module.exports = class MontyPython {
getSillyWalk(numberOfSteps) {
const steps = []
for (let i = 0; i < numberOfSteps; i++) {
const previousStep = steps[i - 1]
if (previousStep === 'left') {
steps.push('right')
} else {
steps.push('left')
}
}
// shuffle
return steps.sort(() => 0.5 - Math.random())
}
}
Since this is randomized functionality, we have to mock its implementation if we need predictable behavior in our tests.
There are three types of mocking modules. Let’s start from local to global:
Mocking per test
Sometimes you want to implement a certain modules differently multiple times within the same file. In this case you should use jest.doMock
followed by requiring affected modules.
describe('MontyPython', () => {
describe('getSillyWalk', () => {
it('returns a series of steps for each leg', () => {
jest.doMock('./monty-python', () => {
return class MontyPython {
getSillyWalk() {
return [
'left', 'right',
'left', 'right',
'left', 'right',
]
}
}
})
const MontyPython = require('./monty-python')
const montyPython = new MontyPython()
expect(montyPython.getSillyWalk(6)).toMatchSnapshot()
jest.resetModules()
})
})
})
To prevent tests from affecting each other, make sure to clean up by call jest.resetModules
. I usually put this in afterEach
, just so I don’t have to always remember to do it, just like cleanup
in react-testing-library.
Mocking per file
While jest.doMock
can also be used on a per file basis, I recommend using the top-level jest.mock
instead. Use it when you need the same mocked implementation across multiple tests in the same file.
This is a special utility that gets hoisted to the top, before all import
statements and require
calls.
const MontyPython = require('./monty-python')
jest.mock('./monty-python', () => {
return class MontyPython {
// mocked implementation
}
})
☝️ The code above actually runs in the reverse order:
jest.mock('./monty-python', () => {
return class MontyPython {
// mocked implementation
}
})
const MontyPython = require('./monty-python')
So the imported MontyPython
class will be the one you provided as mocked implementation (a.k.a. factory) in the jest.mock
call. Keep this in mind to avoid unexpected behavior. Sometimes errors will remind you about this, e.g. if you try to do funny business like this:
const { meaningOfLife } = require('./consts')
jest.mock('./monty-python', () => {
return class MontyPython {
getTheMeaningOfLife() {
return meaningOfLife
}
}
})
Jest will throw an error and explaning why this won’t work:
babel-plugin-jest-hoist: The module factory of `jest.mock()` is not allowed to reference any out-of-scope variables.
Invalid variable access: meaningOfLife
Whitelisted objects: Array, ArrayBuffer, Boolean, (...)
Note: This is a precaution to guard against uninitialized mock variables. If it is ensured that the mock is required lazily, variable names prefixed with `mock` (case insensitive) are permitted.
Other than this caveat, jest.mock
is pretty much the same as jest.doMock
, with obvious difference that the scope is now the whole file, not a single test. Also, you don’t need to reset modules because they are being reset automatically for each test file.
If there is a certain test where you want to use the real monty-python
module, you can do so using jest.requireActual
:
const MockedMontyPython = require('./monty-python')
jest.mock('./monty-python', () => {
return class MontyPython {
getTheMeaningOfLife() {
return 42
}
}
})
describe('MontyPython', () => {
describe('getTheMeaningOfLife', () => {
it('gets the mocked meaning of life', () => {
const montyPython = new MockedMontyPython()
expect(montyPython.getTheMeaningOfLife()).toBe(42)
})
it('gets the real meaning of life', () => {
const RealMontyPython = jest.requireActual('./monty-python')
const montyPython = new RealMontyPython()
const mathRandomSpy = jest.spyOn(Math, 'random')
mathRandomSpy.mockImplementation(() => '¯\_(ツ)_/¯')
expect(montyPython.getTheMeaningOfLife()).toBe('¯\_(ツ)_/¯')
mathRandomSpy.mockRestore()
})
})
})
Alternatively you can use jest.dontMock
, followed by a regular require
call:
it('gets the real meaning of life', () => {
jest.dontMock('./monty-python')
const RealMontyPython = require('./monty-python')
// the rest is the same as with jest.requireActual()
const montyPython = new RealMontyPython()
const mathRandomSpy = jest.spyOn(Math, 'random')
mathRandomSpy.mockImplementation(() => '¯\_(ツ)_/¯')
expect(montyPython.getTheMeaningOfLife()).toBe('¯\_(ツ)_/¯')
mathRandomSpy.mockRestore()
jest.resetModules()
})
Lastly, passing the implementation to jest.mock
is actually optional, I lied by omission! If you don’t pass the implementation, the default behavior replaces all functions in that module with dummy mocks, which I don’t find particularly useful, but things get more interesting when you add a __mocks__
folder. 👇
Mocking globally
Jest calls these “manual mocks”. Personally, I use them rarely, but they’re handy when you want to mock a certain module in multiple test files. You can create them by using the following file structure:
monty-python.js
__mocks__
└── monty-python.js
It works by placing a __mocks__
folder right next to the module you’re mocking, containing a file with the same name, and its content will be used as the mocked implementation:
// __mocks__/monty-python.js
module.exports = class MontyPython {
getTheMeaningOfLife() {
return 42
}
}
Now when you call jest.mock('./monty-python')
without providing an inline implementation Jest will use the manual mock:
const MockedMontyPython = require('./monty-python')
// no implementation specified, using the manual mock
jest.mock('./monty-python')
// ...
You can always override the manual mock as needed by passing in an inline implementation.
To add a manual mock for an npm package or a built-in module like fs
or path
you need to add the __mocks__
folder next to node_modules
, most often in the root of your project. Manul mocks for npm packages will be used automatically, even without calling jest.mock
(this doesn’t apply to built-in modules). Jest documentation presents this behavior as a feature, but I see it as a relic from their former behavior when they were automocking all modules by default.
You can always opt-out from manual mocks in lots of different ways, depending on what you need:
-
by passing the implementation to
jest.mock
, -
by calling
jest.unmock
for modules like those innode_modules
that would otherwise be mocked automatically, -
by calling
jest.requireActual
orjest.dontMock
, if you need to use actual implementation only in particular tests, not the whole file etc.
Conclusion
It took me a long time to understand the nuances of these features, how to get what I want and how to even know what I want. I encourage you to scroll through the jest object reference to learn more about these features and how they compare to the ones that I didn’t cover in this post. If you catch yourself repeating the same module implementation multiple times, try saving some work by using a different mocking approach.
I hope that this post brought you some clarity on the subject, have fun building better tests!