Modules
Loading "Intro to Modules"
Run locally for transcripts
Mocking modules should be your last resort because it's the most intrusive mocking technique out there. As we've previously covered, mocking focuses on either values or behaviors. When mocking a module, you are throwing away both, and leave it up to your mock to fill in the gaps in the best way possible.
In practice, mocking modules may harm more than it may help. You rarely want to mock an entire module. You are problably thinking of a particular value or a behavior introduced by that module, and you should be mocking that instead, using the techniques you've already learned at this point.
Case study: Mocking Axios
A common example of module mockingin the wild is mocking request clients, like
axios
. Let's take a look at how that works, what problems that has, and why it may not be the best idea.Here's a
fetchUser()
function that depends on axios
to get the user by ID:import axios from 'axios'
export async function fetchUser(userId: string) {
const response = await axios(`https://example.com/user/${userId}`)
return response.data
}
What is often proposed is to mock the
axios
dependency in a test in order to remove the actual HTTP request from the testing equation and gain control over the returned response.import axios from 'axios'
import { fetchUser } from './fetch-user.js'
vi.mock('axios')
beforeEach(() => {
vi.resetAllMocks()
})
beforeAll(() => {
vi.restoreAllMocks()
})
test('returns the user by ID', async () => {
axios.get.mockResolvedValue({
data: { id: 'abc-123', name: 'John' },
})
await expect(fetchUser('abc-123')).resolves.toEqual({
id: 'abc-123',
name: 'John',
})
})
The implications of mocking the
axios
module in this test are as big as they are unapparent:- Tight coupling. If the tested code switches from
axios.get()
to (imaginary)axios.request()
tomorrow, the test will fail. This is a by-the-book example of a brittle test that violates the Golden Rule of Assertions. - Black-box testing. By mocking the entire
axios
package, we are throwing away everything that Axios does. Granted, we do want to throw away some of those things, like actually making the request, but there's a lot of logic going on around the request that we really want to preserve in a test—request validation, response normalization, etc.
Those are enough reasons to scrap this test and start over.
Now that you know what is the true purpose of mocking, you can take a step back and ask yourself a question: What is it I really need to mock in this test? The answer will be twofold:
- Remove the HTTP request from the test (i.e. behavior);
- Control what response is returned from the server (i.e. value).
The correct approach to orchestrating such a test for the
fetchUser()
function is to mock the HTTP request and leave the axios
out of the picture entirely. Your users don't know that you depend on Axios, and neither should your tests.Compare the previous example with a test that uses API mocking instead:
import axios from 'axios'
import { http } from 'msw'
import { setupServer } from 'msw/node'
import { fetchUser } from './fetch-user.js'
vi.mock('axios')
const server = setupServer(
http.get('https://example.com/user/:userId', () => {
return Response.json({
id: 'abc-123',
name: 'John',
})
}),
)
beforeAll(() => server.listen())
afterAll(() => server.close())
test('returns the user by ID', async () => {
axios.get.mockResolvedValue({
data: { id: 'abc-123', name: 'John' },
})
await expect(fetchUser('abc-123')).resolves.toEqual({
id: 'abc-123',
name: 'John',
})
})
This test no longer knows aboutaxios
, doesn't couple itself with the{ data: T }
formataxios.get()
has to resolve with, and, most importantly, runs the rest of Axios code intact.
The argument for module mocking
That being said, mocking modules is still a technique you need to know. Mostly, to know of its dangers, but also because there are cases when mocking a module is a preferrable or, sometimes, even the only suitable approach. Let's dive into those cases!