What is Mock Hoisting & Why do We Need It?
In vitest, mock hoisting refers to the behavior where calls to vi.mock
are hoisted to the top of the file during test execution. This means that the mock setup defined using vi.mock
will take effect before any other code in the file, including variable declarations and other imports.
Here’s an example to illustrate how mock hoisting works:
// This vi.mock call will be hoisted to the top of the file during test execution
vi.mock('./some-module', () => ({
someFunction: vi.fn(() => 'mocked result'),
}));
// This variable declaration will be hoisted below the vi.mock call
const myVariable = 'some value';
// This import statement will also be hoisted below the vi.mock call
import SomeComponent from './SomeComponent';
// During test execution, the mock setup will take effect first, then the variable
// declaration and imports will be processed.
JavaScriptUnderstanding mock hoisting is important because it ensures that your mock setups are applied before the code being tested is executed. This allows you to effectively mock dependencies and control the behavior of your code during testing.
While mock hoisting helps ensure the mock setups for our tests, it also causes confusion for developers who lack a full understanding of it.
Unintended Issues of Mismanaged Mock Hoisting
In the early days of writing my mock functions, I encountered some peculiar bugs that resulted in hours of debugging and even led to smashing a few monitor screens.
Take a look at the code example below:
// actions.ts
export const getNumber = () => 0
JavaScript// actions.test.ts
import {getNumber} from './actions'
import { expect, test, vi } from 'vitest'
test('Test A',() => {
vi.mock('./actions', () => {
return {
getNumber: () => 1
}
})
expect(getNumber()).toBe(1)
})
test('Test B',() => {
vi.mock('./actions', () => {
return {
getNumber: () => 2
}
})
expect(getNumber()).toBe(2)
})
test('Test C',() => {
vi.mock('./actions', () => {
return {
getNumber: () => 3
}
})
expect(getNumber()).toBe(3)
})
test('Test D',() => {
vi.mock('./actions', () => {
return {
getNumber: () => 3
}
})
expect(getNumber()).toBe(3)
})
JavaScriptSeems straightforward, right? Well, brace yourself for the test result—it may surprise many developers who are new to Vitest mocking.
Test A × expected 1 but received 3.
Test B × expected 2 but received 3.
Test C ✓ received 3, passed.
Test D ✓ received 3, passed.
If we apply mock hoisting to the code, the equivalent code would be:
// actions.test.ts
// The following mocks are hoisted to the top but are overwritten by subsequent ones.
// vi.mock('./actions', () => {
// return {
// getNumber: () => 1
// }
// })
// vi.mock('./actions', () => {
// return {
// getNumber: () => 2
// }
// })
// vi.mock('./actions', () => {
// return {
// getNumber: () => 3
// }
// })
// At the end, only this mock remains.
vi.mock('./actions', () => {
return {
getNumber: () => 3
}
})
import {getNumber} from './actions'
import { expect, test, vi } from 'vitest'
test('Test A',() => {
expect(getNumber()).toBe(1)
})
test('Test B',() => {
expect(getNumber()).toBe(2)
})
test('Test C',() => {
expect(getNumber()).toBe(3)
})
test('Test D',() => {
expect(getNumber()).toBe(3)
})
JavaScriptWith the transformation of the original code, we can now see exactly what’s happening behind the scenes. Understanding the cause is one thing, but how do we fix it? How do we apply different mock values to different tests?
Solution
We can take advantage of the function mockImplementationOnce()
. Here’s an example of the solution code:
import { getNumber } from './actions'
import { expect, test, vi } from 'vitest'
beforeAll(async () => {
vi.spyOn(await import('./actions'), 'getNumber')
// Provide a mock implementation that returns 1 for the first call
.mockImplementationOnce(() => 1)
// Provide a mock implementation that returns 2 for the second call
.mockImplementationOnce(() => 2)
// Provide a default mock implementation that returns 3
//for rest of the calls
.mockImplementation(() => 3)
})
test('Test A', () => {
expect(getNumber()).toBe(1)
})
test('Test B', () => {
expect(getNumber()).toBe(2)
})
test('Test C', () => {
expect(getNumber()).toBe(3)
})
test('Test D', () => {
expect(getNumber()).toBe(3)
})
JavaScriptThis saves the day swiftly.