Why Vitest for Mocking
Vitest offers several advantages over other testing frameworks when it comes to mocking. First, its deep integration with Vite means that module mocking works seamlessly with ESM imports, TypeScript, and other features that modern projects rely on. Unlike Jest, which uses a custom module system that can conflict with Vite's approach, Vitest's mocking system is built from the ground up to work with Vite's transformation pipeline. This means you get accurate source maps, proper TypeScript support, and consistent behavior whether you're running in Node, JSDOM, or an actual browser environment.
Second, Vitest's mocking API is more intuitive and flexible than its predecessors. The framework provides clear distinctions between different types of mocking operations:
vi.mock()replaces entire modules with custom implementationsvi.spyOn()tracks function calls while preserving original behaviorvi.fn()creates standalone mock functions for flexible testing
Understanding when to use each of these tools--and how to combine them effectively--is essential for writing tests that are both comprehensive and maintainable. Our web development services team regularly implements these patterns to ensure robust, testable codebases for our clients.
Everything you need to mock effectively in your tests
Module-Level Mocking
Replace entire modules with custom implementations using vi.mock() factory functions
Function Spies
Track function calls with vi.spyOn() while preserving original implementation
Class Mocking
Handle ES6 classes and constructor functions with specialized patterns
Async Mocking
Mock promises and asynchronous module imports for modern applications
Environment Support
Works across JSDOM, Node.js, and browser testing environments
Timer Mocking
Control time-dependent tests with fake timers and date mocking
Understanding Module Mocking in Vitest
What Is a Module in Vitest Context
In the context of Vitest, a module is any file that exports values--whether those are functions, classes, objects, or primitive values. Using Vite plugins, virtually any file can be treated as a JavaScript module. The module object itself is a namespace that holds dynamic references to exported identifiers, meaning it tracks changes to exports even after the module has been imported. This dynamic nature is crucial for understanding how Vitest's mocking system works under the hood.
When you import a module, either as a namespace import (import * as exampleObject from './example.js') or as named imports (import { calculateAnswer, configuration } from './example.js'), Vitest creates a module object that maintains references to these exports. This module object exists independently of how you imported the values, which is why Vitest can intercept and mock modules effectively regardless of your import style.
Complete Module Replacement with vi.mock()
The vi.mock() function is Vitest's primary tool for completely replacing a module with a custom implementation. When you call vi.mock() with a module path, Vitest prevents the original module from loading and instead uses whatever you provide in the factory function:
import { vi } from 'vitest';
vi.mock('./example.js', () => {
return {
calculateAnswer: () => 100,
configuration: 'test',
};
});
One important detail is that vi.mock() calls are hoisted to the top of the file during test execution, which means you can call them after import statements in your source code. This hoisting is essential for ensuring mocks are registered before any imports occur.
Factory Functions and Dynamic Mocks
The factory function you pass to vi.mock() can be either synchronous or asynchronous, giving you flexibility in how you create your mocks. For complex scenarios, especially when you need to access the original module's implementation, you can use the importOriginal helper:
import { vi } from 'vitest';
vi.mock('./example.js', async (importOriginal) => {
const originalModule = await importOriginal();
return {
...originalModule,
calculateAnswer: vi.fn().mockReturnValue(42),
configuration: 'mocked',
};
});
This pattern allows you to preserve some functionality from the original module while mocking specific exports--a powerful technique for comprehensive unit testing.
Related Services
For comprehensive testing strategies, consider our Web Development Services which include full testing implementations. We also provide Software Development Services for custom application testing frameworks, and Custom Web Applications Services for tailored testing solutions.
Function Spies and Mock Functions
Creating Mock Functions with vi.fn()
The vi.fn() function creates standalone mock functions that you can use anywhere--passing them as callbacks, assigning them to properties, or returning them from mocked modules. A mock function tracks all calls made to it, along with the arguments passed and values returned, enabling powerful assertions about how functions are used in your tests:
const mockCallback = vi.fn();
mockCallback('arg1', 'arg2');
mockCallback('arg3');
console.log(mockCallback.mock.calls);
// Output: [['arg1', 'arg2'], ['arg3']]
Mock functions provide several methods for configuring their behavior:
mockReturnValue(value)- Set a fixed return valuemockReturnValueOnce(value)- Set return value for one callmockImplementation(fn)- Provide custom implementationmockResolvedValue(value)- Return a resolved promisemockRejectedValue(error)- Return a rejected promise
Tracking Function Calls with spyOn()
While vi.fn() creates new mock functions, vi.spyOn() allows you to track existing methods or properties on objects while preserving their original behavior. This is essential when you want to verify that a function was called without replacing its implementation:
import { vi } from 'vitest';
const user = {
name: 'John',
getName() {
return this.name;
},
};
const spy = vi.spyOn(user, 'getName');
spy.mockReturnValue('Mocked Name');
console.log(user.getName()); // 'Mocked Name'
expect(spy).toHaveBeenCalled();
To restore the original behavior after your test, call spy.mockRestore(). Spying on methods is particularly valuable when testing React components or other scenarios where you want to verify interaction without breaking the actual functionality.
Related Solutions
Spying on module exports is particularly useful for testing React Development Solutions and TypeScript Development Solutions where you want to verify that specific functions are being invoked correctly. These frameworks benefit greatly from proper mocking strategies to ensure component behavior is thoroughly tested.
Class Mocking in Vitest
Understanding Class Mocks
ES6 classes present unique mocking challenges because they combine constructor functions with prototype methods. Vitest handles class mocking through the same vi.mock() mechanism used for modules, but with specific considerations for preserving class structure. When mocking a class, you need to ensure the mock has the correct prototype chain if you're creating instances in your tests.
The recommended approach for mocking classes is to create a factory function that returns a class with the same structure:
import { vi } from 'vitest';
class DataService {
constructor(baseUrl) {
this.baseUrl = baseUrl;
}
async fetchData(id) {
const response = await fetch(`${this.baseUrl}/data/${id}`);
return response.json();
}
}
vi.mock('./DataService', () => {
return {
default: vi.fn().mockImplementation((baseUrl) => ({
baseUrl,
fetchData: vi.fn().mockResolvedValue({ id: 1, name: 'Test' }),
})),
};
});
This pattern uses vi.fn().mockImplementation() to create a constructor mock that returns objects with mocked methods. The key is ensuring the returned object has the same shape as instances of the original class.
Constructor Spying
Sometimes you don't want to replace an entire class but instead want to verify that a class is instantiated correctly or track constructor calls. Vitest allows you to spy on class constructors using vi.spyOn():
import { expect, vi } from 'vitest';
import { DataService } from './DataService';
const constructorSpy = vi.spyOn(DataService, 'constructor');
new DataService('https://api.example.com');
expect(constructorSpy).toHaveBeenCalledTimes(1);
This spy tracks calls to the constructor function, allowing you to verify instantiation patterns without creating actual instances--useful for testing factory functions and dependency injection patterns commonly used in TypeScript applications.
Enterprise Applications
For enterprise-level applications with complex class hierarchies, our Enterprise Software Services provide comprehensive testing strategies including advanced mocking patterns for large-scale systems. These services ensure your enterprise applications maintain high test coverage and reliability.
Advanced Mocking Patterns
Mocking HTTP Requests
A common testing scenario involves code that makes HTTP requests. While you can mock the fetch function or HTTP client library directly, Vitest provides patterns for handling this at the module level. The most robust approach involves creating a mock module that intercepts requests and returns predictable responses:
import { vi } from 'vitest';
// Mock the fetch function globally
global.fetch = vi.fn();
beforeEach(() => {
vi.clearAllMocks();
});
test('fetches user data', async () => {
const mockUser = { id: 1, name: 'John' };
fetch.mockResolvedValueOnce({
ok: true,
json: () => Promise.resolve(mockUser),
});
const result = await fetchUser(1);
expect(fetch).toHaveBeenCalledWith('https://api.example.com/users/1');
expect(result).toEqual(mockUser);
});
Mocking Timers and Dates
Testing time-dependent code requires controlling the passage of time or the current date. Vitest provides timer mocking through vi.useFakeTimers() and date mocking through specific date utilities. These features are essential for testing debouncing, throttling, caching, expiration logic, and any code that depends on Date, setTimeout, setInterval, or requestAnimationFrame:
import { expect, vi } from 'vitest';
test('delayed notification', () => {
vi.useFakeTimers();
const showNotification = vi.fn();
const delayedNotification = (message, delay) => {
setTimeout(() => showNotification(message), delay);
};
delayedNotification('Hello', 1000);
expect(showNotification).not.toHaveBeenCalled();
vi.advanceTimersByTime(1000);
expect(showNotification).toHaveBeenCalledWith('Hello');
vi.useRealTimers();
});
Mocking Environment Variables
Environment variables often control application behavior, and testing different configurations requires mocking these values. Vitest provides vi.env for setting environment variables and vi.mock('node:process') for more comprehensive process mocking:
import { vi } from 'vitest';
test('uses API key from environment', () => {
vi.env.API_KEY = 'test-key-123';
const api = createApiClient();
expect(api.config.headers.Authorization).toBe('Bearer test-key-123');
});
These advanced mocking patterns are essential building blocks for creating reliable test suites in modern JavaScript applications.
Mobile Application Testing
For mobile applications using React Native or other cross-platform frameworks, testing strategies must account for platform-specific APIs. Learn more about our Mobile App Development Services which include comprehensive testing approaches for mobile-specific challenges, ensuring your mobile applications work flawlessly across different platforms.
Mocking in Different Environments
JSDOM and Node Environments
Vitest supports multiple test environments, and mocking behavior can differ between them. In JSDOM and Node environments, Vitest uses a module runner that hooks into module evaluation, allowing it to intercept and replace modules before they execute. This gives Vitest significant flexibility in how it implements mocking, including the ability to "bend the rules" around ES Module immutability that would normally prevent modification of imported values.
In these environments, mocking works by creating a custom module runner that wraps module imports with mock handling. When you call vi.mock(), Vitest registers the mock in its internal registry, and the module runner checks this registry before loading each module. If a mock exists, the runner returns the mock instead of loading the original module.
Browser Mode Considerations
Browser mode in Vitest uses native ESM and cannot replace modules with the same flexibility as JSDOM or Node modes. Instead, Vitest intercepts fetch requests and serves transformed code when modules are mocked. The { spy: true } option becomes particularly important in browser mode:
import { vi } from 'vitest';
import * as exampleObject from './example.js';
vi.mock('./example.js', { spy: true });
vi.mocked(exampleObject.answer).mockReturnValue(0);
expect(exampleObject.answer()).toBe(0);
The { spy: true } option avoids the complexity of module replacement. Instead of creating new mock objects, Vitest injects spies into the existing module, allowing the original code to execute while still tracking calls and return values. This approach is more limited than full module replacement but works reliably across all environments, making it ideal for cross-platform JavaScript development.
Best Practices for Effective Mocking
Mock at the Right Level
Choosing where to apply mocks significantly impacts test quality and maintenance. Mock at the appropriate level for what you're testing:
- Unit tests: Mock direct dependencies of the unit under test
- Integration tests: Mock external services and databases, but allow integration between internal modules
- E2E tests: Avoid mocks entirely, testing the complete system
Over-mocking can make tests fragile and less reflective of actual behavior, while under-mocking can make tests slow, flaky, or dependent on external factors. The goal is to isolate the unit under test while maintaining realistic behavior for everything else.
Keep Mocks Simple
Complex mocks are often harder to maintain and can obscure bugs in your tests. Follow these guidelines:
- Create mocks that return predictable, simple values
- Use
mockReturnValueovermockImplementationwhen possible - Avoid business logic in mocks--mocks should be dumb
- Comment complex mock configurations to explain their purpose
Verify Mock Interactions
Simply setting up mocks is not enough--your tests should verify that mocks are being used correctly:
expect(mockFunction).toHaveBeenCalled();
expect(mockFunction).toHaveBeenCalledWith('expected argument');
expect(mockFunction).toHaveBeenCalledTimes(1);
expect(mockFunction).toHaveReturnedWith('expected value');
Setup Files and Global Mocks
When the same mock is needed across multiple test files, using a setup file is more efficient than repeating the mock definition:
// vitest.setup.ts
import { vi } from 'vitest';
vi.mock('./src/api/client', () => ({
default: {
get: vi.fn(),
post: vi.fn(),
},
}));
vi.env.NODE_ENV = 'test';
Following these best practices ensures your test suite remains maintainable and provides genuine confidence in your software quality.
Troubleshooting Common Mocking Issues
Mock Not Being Applied
If your mock does not seem to be taking effect, check these common issues:
-
Import timing: Make sure
vi.mock()is called before the module is imported. Vitest hoistsvi.mock()calls, but if you are mocking a module that is imported in the setup file or configuration, ensure the mock is defined first. -
Module path accuracy: Verify the mock path matches exactly how the module is imported. Relative paths must match the importing file's location.
-
Factory return structure: Ensure your factory returns an object with the same shape as the original module. Missing exports will cause "export not found" errors.
Spy Not Tracking Calls
If a spy is not recording calls:
-
Check spy creation timing: The spy must be created before the function is called.
-
Verify spy attachment: Ensure you are spying on the correct object and method name.
-
Consider call context: Some functions lose their
thiscontext when passed around. Use.bind(object)or arrow functions to preserve context.
TypeScript and Mocking
TypeScript can provide challenges when working with mocks. The vi.mocked() helper provides type safety by asserting that a value is a mock:
import { mocked } from 'vitest';
mocked(fetch).mockResolvedValueOnce({ ok: true });
// Or use type casting for complex scenarios
const mockFetch = fetch as unknown as vi.Mock;
These troubleshooting tips help you quickly resolve common issues and maintain a healthy test suite for your TypeScript projects.
Conclusion
Mastering Vitest's mocking system is essential for writing effective unit tests in modern JavaScript and TypeScript applications. The framework's deep integration with Vite provides powerful capabilities for mocking modules, functions, and classes at various levels of granularity. By understanding the distinction between:
vi.mock()for module replacementvi.spyOn()for tracking existing functionsvi.fn()for creating standalone mocks
You can build a testing strategy that provides both confidence in your code and fast, reliable test execution. The key to effective mocking lies in choosing the right tool for each scenario and maintaining a balance between isolation and realism.
With these techniques in your toolkit, you will be well-equipped to tackle even the most complex testing scenarios in your applications.
Ready to Implement Advanced Testing?
Our team specializes in implementing comprehensive testing practices for enterprise applications. Explore our Custom Web Applications Services to learn how we can help you implement comprehensive testing strategies including Vitest mocking for your next project. We also offer Enterprise Software Services for large-scale systems and Software Development Services for custom testing frameworks that ensure robust, maintainable codebases.
Frequently Asked Questions
What is the difference between vi.mock and vi.spyOn?
vi.mock() completely replaces a module with a custom implementation, while vi.spyOn() tracks function calls while preserving the original implementation. Use vi.mock() when you need to replace functionality, and vi.spyOn() when you want to verify calls without breaking original behavior.
How do I restore mocks after a test?
Use spy.mockRestore() to restore a single spied function, or vi.restoreAllMocks() to restore all mocked functions. This removes the spy and returns to the original implementation.
Can I mock async functions in Vitest?
Yes, use mockResolvedValue() for successful async results or mockRejectedValue() for errors. You can also use mockImplementation() with an async function for more complex scenarios.
How do I mock environment variables in tests?
Use vi.env.VARIABLE_NAME = 'value' to set environment variables. For the process object, use vi.mock('node:process') to mock the entire process module.
Why is not my mock being applied in browser mode?
Browser mode uses native ESM, which has limitations on module replacement. Use the { spy: true } option with vi.mock() to inject spies instead of replacing modules entirely.