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:
| Pattern | Use Case | Example |
|---|---|---|
vi.mock() | Mock an entire imported module | Mock the axios HTTP library entirely |
vi.spyOn() | Spy on a specific function on an object | Spy 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; usevi.spyOn()for individual functions. - Mock async functions with
.mockResolvedValue()or.mockRejectedValue(). - Use
vi.importActual()insidevi.mock()to selectively mock (replace one export, keep others). - Mock global objects like
fetchandlocalStoragewithvi.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.