Articles
Vitest Mocking Interview Questions: Hoisting, Module State, and Fake Timers
Practice Vitest mocking questions on vi.mock hoisting, clear vs reset vs restore, module cache, fake timers, and Browser Mode limits.
Vitest mocking interview questions become difficult when several kinds of state look like one problem. A test fails only after another test runs, so a candidate adds restoreAllMocks() and expects the suite to become clean. The failure remains because the surviving state is a module cache or fake clock rather than a spy descriptor.
Quick answer: first classify the leak. clearAllMocks clears call history. resetAllMocks also resets mock behavior to each mock's initial implementation. restoreAllMocks restores descriptors replaced through vi.spyOn, but does not clear their history. resetModules clears the module registry for later dynamic imports. useRealTimers restores the clock. None is a universal reset button.
This guide separates current official Vitest behavior, our Vitest 5.0.0 lab results, and editorial interview guidance.
Why does vi.mock run before your setup code?
Interview question: Predict the execution order
Consider this test:
import { total } from './price.js'
const rate = vi.fn(() => 2)
vi.mock('./rate.js', () => ({ getRate: rate }))
It reads as though rate is created before the mock call. That is not the runtime order Vitest creates.
Official behavior: Vitest statically transforms files containing vi.mock, moves the mock registration before imports, and turns static imports into controlled dynamic imports. The factory therefore cannot safely close over an ordinary top-level variable initialized later. The API documentation recommends vi.hoisted for values needed by a mock factory. vi.mock documentation.
The repaired pattern is:
const hoisted = vi.hoisted(() => ({
getRate: vi.fn(() => 2),
}))
vi.mock('../src/rate.js', () => ({
getRate: hoisted.getRate,
}))
import { total } from '../src/price.js'
Our Node 24.19.0, Vitest 5.0.0 run logged HOIST total=20 mockCalls=1 for a subtotal of 10. That output proves the fixture, not every loader or browser environment.
Why not move vi.mock into beforeEach?
Putting vi.mock textually inside beforeEach does not make it runtime-local; the call is hoisted. Use vi.doMock when you deliberately need a non-hoisted mock for a subsequent dynamic import. Then state which imports have already been evaluated. A top-level consumer imported earlier retains its existing binding and evaluation history.
An interview answer should explain the dependency graph: mock registration must happen before the consumer module evaluates its imports. vi.hoisted solves factory initialization, while dependency injection can make the production boundary simpler to test.
What is the difference between clear, reset, and restore?
The names are easy to memorize and easy to misuse. Ask two questions: what state changes, and what happens on the next call?
| Operation | Changes | Does not solve |
|---|---|---|
vi.clearAllMocks() | Calls .mockClear() on spies; removes calls, results, and related history | Temporary implementations, descriptors, modules, timers |
vi.resetAllMocks() | Calls .mockReset(); clears history and resets each implementation | Module cache, fake clock, environment stubs |
vi.restoreAllMocks() | Restores original descriptors for vi.spyOn spies | Mock history, automocks, modules, timers |
Official behavior: current Vitest describes these responsibilities separately and warns that restoreAllMocks does not clear history or reset mock implementation. vi cleanup APIs.
A Vitest 5 result candidates often miss
In our verified fixture:
const fn = vi.fn(() => 'original')
fn.mockImplementation(() => 'temporary')
fn('before')
vi.resetAllMocks()
console.log(fn.mock.calls.length, fn()) // 0, 'original'
The initial implementation passed to vi.fn(impl) is the reset target. Current mock API documentation distinguishes that from vi.fn() with no implementation, which resets to an empty function returning undefined. Vitest mockReset.
Our other checks logged:
CLEAR calls=0 result=kept
RESET calls=0 result=original
RESTORE result=6
The restore case spied on a real multiply(2, 3) export, temporarily returned 9, then returned the original result 6 after restoration. Do not test restoration by spying on an export from a module already replaced by vi.mock; its “original” at that point may already be the mocked export.
Why does module state survive between tests?
Interview question: Why did resetAllMocks not reset a counter?
A module can own ordinary JavaScript state that is not mock state:
let count = 0
export const increment = () => ++count
Calling resetAllMocks has no reason to re-evaluate this file. It only operates on mocks and spies. Official behavior: vi.resetModules() clears the module registry so a later import can be reevaluated, but top-level imports cannot be reevaluated. It also does not reset the mocks registry. vi.resetModules.
Our lab dynamically imported the counter, observed 1 then 2, reset modules, imported it again, and observed 1. The log was MODULE beforeReset=2 afterReset=1.
Use this technique selectively. Rebuilding the module graph in every test can make a suite slower and can hide a design problem. If a module-level cache is intentional, expose an explicit lifecycle or construct an instance per test. If it is accidental, remove it. In an interview, say why the state exists before choosing a reset.
Why can setup files defeat a mock?
The official vi.mock docs warn that modules imported in a setup file are already cached by the time a test file runs. A later mock cannot retroactively replace work that already happened. This is another dependency-order problem. Inspect setup imports and use a focused test project rather than clearing every module without understanding the graph.
How do fake timers interact with promises and cleanup?
Interview question: Why did advancing time not finish the callback?
A timer callback may schedule promise work. The synchronous timer APIs advance scheduled timers, while async variants also wait through asynchronously scheduled timer work. Our fixture uses:
vi.useFakeTimers()
const callback = vi.fn()
scheduleStatus(callback) // setTimeout; callback awaits Promise.resolve()
await vi.advanceTimersByTimeAsync(1000)
expect(callback).toHaveBeenCalledWith('ready')
Vitest 5 logged TIMER calls=1 value=ready. The official API says advanceTimersByTimeAsync includes asynchronously set timers. Timer advancement API.
Always restore real timers in afterEach or a finally block. Official behavior: useFakeTimers wraps subsequent timer calls and Date until useRealTimers; restoring real timers discards timers that were scheduled under the fake clock. Fake timer API.
That last detail affects cleanup order. If the test must observe a pending callback, advance or explicitly clear it before restoring the clock. Do not blindly call runAllTimers on an application with a self-scheduling poller; use a bounded advancement and assert the intended transition.
Which mocking behaviors change in Browser Mode?
Our executable fixture used Vitest's Node environment. Official Browser Mode limitation: browser-native ESM namespace objects are sealed and cannot be reconfigured the same way as modules handled by the Node-style module runner. Vitest documents vi.mock('./module.js', { spy: true }) as the supported way to keep implementations while tracking exports in Browser Mode. Mocking modules in Browser Mode.
Do not generalize a Node vi.spyOn(namespace, 'export') success to Browser Mode. State the environment before explaining the API. Browser Mode also changes what counts as an integration boundary: native module loading and real browser APIs can be part of the evidence, while jsdom emulates them.
This does not mean Browser Mode is always preferable. Choose it when browser behavior is the risk under test. Keep pure calculation and small orchestration tests in the faster environment that faithfully exercises their contract.
A five-state debugging method
When a test passes alone and fails in the suite, inventory state before changing global configuration:
- History: is the assertion seeing calls from a prior test?
- Implementation: did a
mockReturnValueor one-time behavior survive? - Descriptor: is the real object method still replaced by a spy?
- Module: did an evaluated singleton, cache, or top-level side effect survive?
- Clock: are fake timers, fake
Date, or pending callbacks still active?
Reproduce the smallest failing pair and log the value before cleanup. Then apply the narrowest lifecycle fix. Global resetMocks can make a suite green while obscuring a helper that mutates shared behavior. The goal is a clear owner for every stateful object.
Walk through one suite-only failure
Suppose test A changes a payment client spy to reject, while test B expects the client's real implementation. B passes alone and fails after A. Begin by proving the order: run A then B in one file and log whether the export is still a mock before B acts. If the descriptor remains replaced, clearAllMocks will only erase A's recorded calls; B will still receive the rejection behavior. Restoring the spy after A returns the method descriptor to its real implementation.
Now change the example: A imports a configuration module after setting an environment value, and B changes the environment before using the already imported configuration. Restoring spies cannot re-run that module's top-level code. B needs a later dynamic import after resetModules, or the production design should read configuration through an explicit object rather than a cached implicit global.
Finally, if A enables fake timers and omits cleanup, B may wait on a timer that never advances. A module reset still leaves the fake clock active. Restore real timers in test cleanup, and decide what should happen to callbacks still scheduled in the fake queue before discarding them.
This progression is a useful interview answer because the same symptom has three different causes. State the observation, disprove two wrong cleanup choices, apply the API that owns the surviving state, and add a regression pair. If the interviewer asks for a configuration-wide fix, discuss whether automatic clearMocks, mockReset, or restoreMocks improves the default without hiding deliberate per-test behavior.
Vitest mocking interview questions with concise answers
Does restoreAllMocks make the whole test environment clean?
No. It restores descriptors for vi.spyOn spies. Current Vitest says it does not clear history, does not affect automocks, and does not reset modules or timers.
Can resetModules reevaluate a top-level import?
No. It affects modules imported after the reset, commonly through dynamic import. It does not reset the mock registry.
When should you use vi.hoisted?
Use it when a hoisted mock factory needs a locally created value or when a side effect must happen before transformed imports. Do not access ordinary imported bindings inside it before initialization.
Why use the async timer APIs?
Use them when timer execution schedules promise or asynchronous timer work that must settle before the assertion. Keep advancement bounded and restore the real clock.
Is Vitest behavior identical to Jest?
No. Their APIs overlap, but module transformation, ESM behavior, versions, and reset semantics can differ. Answer from the named runner's current documentation and a pinned reproduction.