Modules

Loading "Intro to Modules"
Mocking modules should be a last resort, as it’s the most intrusive mocking technique. As we've previously covered, mocking typically targets either values or behaviors. When you mock a module, you discard both, leaving your mock to fill in the gaps as best it can.
In practice, mocking entire modules often causes more harm than good. You rarely need to mock an entire module; usually, you’re focused on a specific value or behavior it introduces. Instead, use the techniques you’ve already learned to mock only what you need.

Case study: Mocking Axios

A common example of module mocking in 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 a 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 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:
  1. 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.
  2. 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 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:
  1. Remove the HTTP request from the test (i.e. behavior);
  2. 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 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 about axios, doesn't couple itself with the { data: T } format axios.get() has to resolve with, and, most importantly, runs the rest of Axios code intact.

The argument for module mocking

That said, mocking modules is still a technique worth knowing—not only to understand its risks but also because, in some cases, it’s the preferred or even the only viable approach. Let’s dive into those cases!