You wrote a mock. The test still hits the real module. Nothing in the error message points at why.
import { fetchUser } from './api'
import { vi, test, expect } from 'vitest'
const mockFetch = vi.fn()
vi.mock('./api', () => ({ fetchUser: mockFetch })) // ReferenceError
What actually happens
vi.mock is hoisted to the top of the file by Vitest's transform, above every import. So by the time the factory runs, mockFetch does not exist yet.
Order at runtime:
-
vi.mock('./api', factory)— registered first -
import { fetchUser } from './api'— resolved, factory invoked -
const mockFetch = vi.fn()— too late
The fix
Use vi.hoisted so the mock value is created in the same hoisted phase:
const { mockFetch } = vi.hoisted(() => ({ mockFetch: vi.fn() }))
vi.mock('./api', () => ({ fetchUser: mockFetch }))
Or skip the shared variable entirely and reach for the mocked module inside the test:
import { fetchUser } from './api'
vi.mock('./api')
test('calls the api', async () => {
vi.mocked(fetchUser).mockResolvedValue({ id: 1 })
// ...
})
The rule worth memorising
Anything referenced inside a
vi.mockfactory must be created byvi.hoisted, or defined inside the factory itself.
That one line prevents most of the mocking bugs people file against Vitest.
Want the full picture?
I cover this end-to-end in my Udemy course: Vitest by Example: 50 Hands-on Drills for TypeScript Unit Testing
10h / Beginner to Intermediate
The link above includes a discount coupon, valid until 2026-08-31. After that it works as a regular course link.
Top comments (0)