Skip to main content
Juliano Alves
Back to blog

Vitest mocking patterns that stay maintainable

3 min read
By Juliano Alves

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.

© 2026 Juliano Alves. All rights reserved.