Mocking Fundamentals
This section implements mocking in vanilla JS without help from testing frameworks and helper functions like Jest. The goal is to better understand how mocking works behind the scenes.
What is Mocking?
The idea behind mocking comes when you have some module that is too expensive to use directly. For example, there could be asynchronous functions involved, or it could be processing a credit card payment, which is too expensive to actually test.
So, you create a fake or mock version of the module, so you can test the module without incurring the costs.
Monkey-patching: Overriding Object Properties
The process of monkey-patching is to
Override the object method with your own mock method.
Cleanup that mock method by reassigning the key to the original method after you're done your test. (This is so other tests aren't affected by what happens in the target test.)
In the example below, we have a utils
object with helper functions that we need to monkey-patch:
Creating a Mock Factory Function
Jest version
A mock function is a special function that stores properties as it's called, making the function easier to test. We will be recreating some of the functionality of jest.fn
.
In Jest, here's some cool things you can do:
Our version
Here's a bare-bones mock factory function. It creates a mock function that utilizes a user-provided implementation of that function. That implementation replaces the original implementation:
We want to add the following features to our mock function:
Stores number of times it's been called.
Stores the arguments passed into each call.
Now we can perform tests to make sure our mock function ran as many times as we expected and with the arguments we expected.
Upgrading Our Monkey-patch with spyOn
Our monkey-patch solution of storing originalWinner
and restoring it during cleanup is a bit messy. We can instead use spyOn
and mockRestore
to do that for us.
Jest version
In Jest, here's the code:
Our version
To recreate the above functionality, here are the requirements:
spyOn
needs to override the original function with a mock function generated byfn
.Note:
fn
needs to accept zero arguments here because the custom implementation comes after.
We need a
mockRestore
method that resets the function back to its original state.We need a
mockImplementation
method that updates and/or sets the custom implementation.
Note: Our solution below uses closures to solve 2 and 3.
Mocking a Module
As it turns out, monkey-patching only works with CommonJS. It doesn't work for ES6 modules.
Jest version
To solve this, Jest introduces the mock
method. It has 2 parameters:
Relative path to module, and
Module factory function that returns a mocked version of the module.
Our version
The way that Jest kind of implements mock
behind the scenes is to utilize require.cache
, an object that stores information about modules in node. Then when you import that module into your file, it will import the mock version instead.
The goal in our vanilla version is to basically
Manually inject our mock module into
require.cache
, making sure to include our mock functions as well.Delete the mock module from
require.cache
after we're done testing.
Things to note:
require.resolve
gets the path for the file.We return a
cleanup
function to remove the mocked module.The mocked module needs to be created before importing it into the file.
Pro tip: Jest actually will run mock
before any other line of code when running your tests for you. This is a convenience it provides, which is especially useful given that ES modules are always hoisted to the top of the file.
Sharing Mocked Modules
Very often, you use a module in multiple test files. In this case, you want to externalize your mocked module and re-use it across tests.
Jest version
Jest allows you to share your mocked module by accessing a __mocks__
directory at the same directory level as your module file.
When you call jest.mock
, it will pull your module from the __mocks__
directory instead of the original file.
Our version
To implement this ourselves, we need to do the following in our test:
Cache the mocked module by running
require
. This will make it show up inrequire.cache
.Add the mocked module to
require.cache
at the real module's path.Finally, when we
require
our module, it will import the mocked module instead.
And that's it! (Of course, this is a huge simplification of what Jest is actually doing. Jest takes full control of the module system.)
Last updated