Vitest mocking patterns that stay maintainable
Vitest’s API mirrors Jest but runs on Vite’s transform pipeline—fast watch mode and native ESM. Mocking still requires discipline: over-mocked suites break on refactors and hide integration bugs.
vi.mock is hoisted
import { describe, it, expect, vi } from 'vitest';
vi.mock('./payment', () => ({
charge: vi.fn().mockResolvedValue({ ok: true }),
}));
import { charge } from './payment';
import { checkout } from './checkout';
it('checkout succeeds', async () => {
await expect(checkout()).resolves.toBe('done');
expect(charge).toHaveBeenCalledOnce();
});
Place vi.mock at the top of the file; Vitest hoists it. Import the module after mock declaration in ESM-friendly patterns per docs.
vi.spyOn for partial mocks
When you only need to stub one method on a real module, vi.spyOn(mod, 'fn').mockImplementation(...) preserves other exports and reduces drift.
Fake timers
vi.useFakeTimers();
vi.setSystemTime(new Date('2026-01-01'));
// …test…
vi.useRealTimers();
Always restore timers in afterEach to avoid polluting other files.
Prefer real fetch with MSW
For HTTP-heavy code, Mock Service Worker intercepts at the network layer—tests exercise real fetch code paths instead of stubbing internal helpers.
Summary
Use narrow mocks (spies, MSW) and reserve full vi.mock for true boundaries (third-party SDKs, time). Fast unit tests are useless if they only prove the mock works.