Skip to main content

Module Mocking in Vitest: How-to Guide

Mocking in tests means replacing real dependencies with fakes so you can test your code in isolation and control external behavior. When testing a React component that calls an API, you mock fetch. When testing a module that imports another module, you mock the import. Vitest provides vi.mock() and vi.spyOn() for these scenarios (Vitest documentation, 2026). This article teaches you every mocking pattern you'll encounter.

vi.mock vs. vi.spyOn: When to Use Each

vi.mock() replaces an entire module at import time. vi.spyOn() wraps a function without replacing the module. Use them for different scenarios:

PatternUse CaseExample
vi.mock()Mock an entire imported moduleMock the axios HTTP library entirely
vi.spyOn()Spy on a specific function on an objectSpy on console.log calls
vi.mock() + vi.importActual()Mock selectively (keep some exports, replace others)Mock only one function from a large utility module

Pattern 1: Mock an Entire Module

The most common pattern. Let's mock an API module:

// api/userService.ts
export async function fetchUser(id: string) {
const res = await fetch(`/api/users/${id}`);
return res.json();
}

export async function updateUser(id: string, data: unknown) {
const res = await fetch(`/api/users/${id}`, {
method: 'PUT',
body: JSON.stringify(data),
});
return res.json();
}

Test the component that uses it:

// __tests__/UserProfile.test.tsx
import { describe, it, expect, vi } from 'vitest';
import { render, screen, waitFor } from '@testing-library/react';
import { UserProfile } from '../components/UserProfile';

// Mock the entire module
vi.mock('../api/userService', () => ({
fetchUser: vi.fn(),
updateUser: vi.fn(),
}));

// Import mocked functions
import * as userService from '../api/userService';

describe('UserProfile', () => {
it('displays user data after fetching', async () => {
// Type-safe mock setup
const mockFetchUser = vi.mocked(userService.fetchUser);
mockFetchUser.mockResolvedValue({ id: '123', name: 'Alice' });

render(<UserProfile userId="123" />);

// Wait for the component to fetch and render
await waitFor(() => {
expect(screen.getByText('Alice')).toBeInTheDocument();
});

// Assert the API was called correctly
expect(mockFetchUser).toHaveBeenCalledWith('123');
});
});

Key points:

  • Place vi.mock() at the top level (not inside tests).
  • Mock return values with .mockResolvedValue() for async functions.
  • Use vi.mocked() to cast the mock for TypeScript type safety.
  • Assert that mocked functions were called with toHaveBeenCalledWith().

Pattern 2: Selective Module Mocking (Mock One Export, Keep Others)

When a module has many exports and you want to mock only one:

// utils/api.ts
export function normalizeUserData(data: unknown) { /* ... */ }

export function parseErrorMessage(err: unknown) { /* returns string */ }

export async function fetchAPI(url: string) { /* real fetch */ }

Mock only fetchAPI:

vi.mock('../utils/api', async () => {
// Keep the real implementations
const actual = await vi.importActual('../utils/api');

return {
...actual,
// Override only fetchAPI
fetchAPI: vi.fn(() => Promise.resolve({ success: true })),
};
});

Now normalizeUserData and parseErrorMessage work as normal, but fetchAPI is mocked.

Pattern 3: Spy on Built-in Functions

Use vi.spyOn() to spy on built-in functions like console.log or Date.now():

it('logs errors to console', () => {
const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});

// Code that calls console.error
handleError(new Error('Test error'));

expect(consoleErrorSpy).toHaveBeenCalledWith('Test error');

// Clean up (not required in Vitest—it auto-cleans spies)
consoleErrorSpy.mockRestore();
});

Pattern 4: Mock Global Objects (fetch, localStorage, etc.)

Mock fetch or localStorage globally:

beforeEach(() => {
// Mock fetch globally
vi.stubGlobal('fetch', vi.fn(() =>
Promise.resolve(
new Response(JSON.stringify({ data: 'test' }), { status: 200 })
)
));

// Mock localStorage globally
vi.stubGlobal('localStorage', {
getItem: vi.fn((key: string) => (key === 'theme' ? 'dark' : null)),
setItem: vi.fn(),
removeItem: vi.fn(),
clear: vi.fn(),
});
});

Every test in the suite now has mocked fetch and localStorage.

Pattern 5: Mock with Different Implementations per Test

Set up a mock, then change its implementation in individual tests:

const mockFetch = vi.fn();
vi.stubGlobal('fetch', mockFetch);

describe('API calls', () => {
it('handles success response', async () => {
mockFetch.mockResolvedValueOnce(
new Response(JSON.stringify({ id: 1 }))
);

const result = await fetchUser(1);
expect(result.id).toBe(1);
});

it('handles error response', async () => {
mockFetch.mockResolvedValueOnce(
new Response('Not Found', { status: 404 })
);

await expect(fetchUser(999)).rejects.toThrow();
});

it('handles network error', async () => {
mockFetch.mockRejectedValueOnce(new Error('Network timeout'));

await expect(fetchUser(1)).rejects.toThrow('Network timeout');
});
});

Use .mockResolvedValueOnce() for single-test behavior, or .mockResolvedValue() for all subsequent tests.

Pattern 6: Mock a Module Conditionally Based on Environment

Mock modules only in test, not in production:

// In vitest.config.ts
export default defineConfig({
test: {
globals: true,
},
});

Then in your module:

// utils/analytics.ts
let tracker: any;

if (process.env.NODE_ENV === 'test') {
tracker = { track: vi.fn() };
} else {
tracker = new RealAnalyticsTracker();
}

export const analytics = tracker;

Or use vi.mock() with conditions:

vi.mock('../utils/analytics', () => ({
analytics: {
track: vi.fn(),
},
}));

Practical Example: Mocking a Third-Party Library

Mock the popular axios HTTP library:

// Mock axios
vi.mock('axios', () => {
return {
default: {
post: vi.fn(),
get: vi.fn(),
},
};
});

import axios from 'axios';

describe('UserAPI', () => {
it('posts user data', async () => {
const mockAxios = vi.mocked(axios);
mockAxios.post.mockResolvedValue({ data: { id: 1, name: 'Alice' } });

const user = await createUser({ name: 'Alice' });

expect(user.id).toBe(1);
expect(mockAxios.post).toHaveBeenCalledWith('/api/users', expect.objectContaining({
name: 'Alice',
}));
});
});

Common Mock Assertions

Assert that mocks were used correctly:

const mockFn = vi.fn();

// Assertions
expect(mockFn).toHaveBeenCalled();
expect(mockFn).toHaveBeenCalledTimes(2);
expect(mockFn).toHaveBeenCalledWith('arg1', 'arg2');
expect(mockFn).toHaveBeenLastCalledWith('latest');
expect(mockFn).toHaveBeenNthCalledWith(2, 'second-call');
expect(mockFn).not.toHaveBeenCalled();

// Clear mock state between tests
mockFn.mockClear();

Key Takeaways

  • Use vi.mock() to replace entire modules; use vi.spyOn() for individual functions.
  • Mock async functions with .mockResolvedValue() or .mockRejectedValue().
  • Use vi.importActual() inside vi.mock() to selectively mock (replace one export, keep others).
  • Mock global objects like fetch and localStorage with vi.stubGlobal().
  • Use .mockResolvedValueOnce() for single-test behavior and .mockResolvedValue() for persistent behavior.
  • Assert mock usage with toHaveBeenCalledWith(), toHaveBeenCalledTimes(), etc.

Frequently Asked Questions

When should I use vi.mock vs. vi.spyOn?

Use vi.mock() when you want to completely replace a module or function (the most common case). Use vi.spyOn() when you want to wrap a function and still call the original, or when mocking built-ins like console or Date. For example, spy on console.error to assert errors were logged, but still let the error log normally.

How do I mock a default export vs. a named export?

For default exports: export default function apiCall() {}. Mock with:

vi.mock('../api', () => ({
default: vi.fn(() => Promise.resolve({})),
}));

For named exports: export function apiCall() {}. Mock with:

vi.mock('../api', () => ({
apiCall: vi.fn(() => Promise.resolve({})),
}));

How do I mock a module that returns different values in different tests?

Use .mockResolvedValueOnce() or .mockImplementationOnce():

const mockFn = vi.fn();

mockFn.mockResolvedValueOnce({ data: 1 }); // First call
mockFn.mockResolvedValueOnce({ data: 2 }); // Second call
mockFn.mockResolvedValue({ data: 3 }); // All subsequent calls

await mockFn(); // Returns { data: 1 }
await mockFn(); // Returns { data: 2 }
await mockFn(); // Returns { data: 3 }

How do I test that a callback was called with a specific object?

Use expect.objectContaining() to match partial objects:

expect(mockFn).toHaveBeenCalledWith(
expect.objectContaining({ id: 1, name: 'Alice' })
);

Or expect.any() for type matching:

expect(mockFn).toHaveBeenCalledWith(
expect.any(Function),
expect.any(String)
);

Do I need to manually clean up vi.mock() between tests?

No. Vitest automatically clears all mocks between tests if you have globals: true. If you use vi.clearAllMocks() manually, call it in a beforeEach() hook.

Further Reading