Advanced Guide to Vitest Testing Mocking

Master the art of mocking in Vitest with comprehensive coverage of module replacement, function spies, class mocking, and real-world patterns for testing modern JavaScript and TypeScript applications.

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 implementations
  • vi.spyOn() tracks function calls while preserving original behavior
  • vi.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.

Key Vitest Mocking Capabilities

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 value
  • mockReturnValueOnce(value) - Set return value for one call
  • mockImplementation(fn) - Provide custom implementation
  • mockResolvedValue(value) - Return a resolved promise
  • mockRejectedValue(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 mockReturnValue over mockImplementation when 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:

  1. Import timing: Make sure vi.mock() is called before the module is imported. Vitest hoists vi.mock() calls, but if you are mocking a module that is imported in the setup file or configuration, ensure the mock is defined first.

  2. Module path accuracy: Verify the mock path matches exactly how the module is imported. Relative paths must match the importing file's location.

  3. 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:

  1. Check spy creation timing: The spy must be created before the function is called.

  2. Verify spy attachment: Ensure you are spying on the correct object and method name.

  3. Consider call context: Some functions lose their this context 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 replacement
  • vi.spyOn() for tracking existing functions
  • vi.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.

Need Help with Your Testing Strategy?

Our team of experienced developers can help you implement comprehensive testing practices, including Vitest mocking strategies, for your JavaScript and TypeScript applications.