Vitest Mock Hoisting

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.
JavaScript

Understanding 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)
})
JavaScript

Seems straightforward, right? Well, brace yourself for the test result—it may surprise many developers who are new to Vitest mocking.

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)
})
JavaScript

With 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)
})
JavaScript

This saves the day swiftly.

References

https://github.com/vitest-dev/vitest/discussions/1255

Leave a Reply

Your email address will not be published. Required fields are marked *