Summary
Vitest hoists vi.mock() calls to the top of the file before any module-level variable initializers run. This means a variable initialized with vi.fn() at module scope will be undefined when a vi.mock() factory callback executes, causing flaky tests that silently pass most of the time but occasionally fail depending on execution order.
The existing hoisted-apis-on-top rule enforces that vi.mock() is placed at the top level of the file, but does not catch references to non hoisted variables inside factory callbacks.
The correct fix is to wrap the variable in vi.hoisted(), which hoists the initializer alongside the vi.mock() call.
Invalid
// mockFoo is undefined when the factory runs
const mockFoo = vi.fn();
vi.mock('./foo', () => ({ foo: mockFoo }));
// chained calls are equally unsafe
const mockBar = vi.fn().mockReturnValue('x');
vi.mock('./bar', () => ({ bar: mockBar }));
Valid
// wrapped in vi.hoisted() — safe to reference in the factory
const mockFoo = vi.hoisted(() => vi.fn());
vi.mock('./foo', () => ({ foo: mockFoo }));
// inline declaration inside the factory is also fine
vi.mock('./bar', () => ({ bar: vi.fn() }));
Notes
- I have a working implementation ready and am happy to open a PR if this is something the maintainers would like to add.
- The rule would not flag
vi.fn() variables that are only used outside of vi.mock() factory callbacks, nor variables already wrapped in vi.hoisted().
- Real-world motivation: We encountered this pattern repeatedly in a large monorepo where tests would pass locally but fail intermittently in CI. The root cause was often the same:
vi.fn() variables referenced in vi.mock() factory callbacks without vi.hoisted(). A lint rule would have caught every instance at author time.
Summary
Vitest hoists
vi.mock()calls to the top of the file before any module-level variable initializers run. This means a variable initialized withvi.fn()at module scope will beundefinedwhen avi.mock()factory callback executes, causing flaky tests that silently pass most of the time but occasionally fail depending on execution order.The existing
hoisted-apis-on-toprule enforces thatvi.mock()is placed at the top level of the file, but does not catch references to non hoisted variables inside factory callbacks.The correct fix is to wrap the variable in
vi.hoisted(), which hoists the initializer alongside thevi.mock()call.Invalid
Valid
Notes
vi.fn()variables that are only used outside ofvi.mock()factory callbacks, nor variables already wrapped invi.hoisted().vi.fn()variables referenced invi.mock()factory callbacks withoutvi.hoisted(). A lint rule would have caught every instance at author time.