mocking-strategies
Test mocking strategies with Vitest. Use when mocking dependencies in tests.
When & Why to Use This Skill
This Claude skill provides a comprehensive guide to mastering mocking strategies within the Vitest testing framework. It enables developers to effectively isolate units under test by mocking external dependencies, stubbing API responses, and spying on method calls, ensuring high-quality, predictable, and maintainable test suites for modern JavaScript and TypeScript applications.
Use Cases
- Isolating units of code by mocking external dependencies like APIs, databases, or third-party libraries to ensure tests are fast and deterministic.
- Verifying internal logic and side effects by using spies (vi.spyOn) to track function calls, arguments, and return values.
- Testing time-sensitive operations such as debouncing, throttling, or scheduled tasks using Vitest's fake timer and system time mocks.
- Creating complex test doubles and partial module mocks to simulate specific edge cases or error states in a controlled environment.
| name | mocking-strategies |
|---|---|
| description | Test mocking strategies with Vitest. Use when mocking dependencies in tests. |
Mocking Strategies Skill
This skill covers mocking strategies for Vitest testing.
When to Use
Use this skill when:
- Isolating units under test
- Mocking external dependencies
- Creating test doubles
- Stubbing API responses
Core Principle
MOCK BOUNDARIES, NOT INTERNALS - Mock external dependencies (APIs, databases) not internal implementation.
vi.fn() - Function Mocks
Basic Mock Function
import { vi, describe, it, expect } from 'vitest';
const mockFn = vi.fn();
// Call the mock
mockFn('arg1', 'arg2');
// Assertions
expect(mockFn).toHaveBeenCalled();
expect(mockFn).toHaveBeenCalledWith('arg1', 'arg2');
expect(mockFn).toHaveBeenCalledTimes(1);
Mock Return Values
const mockFn = vi.fn();
// Return specific value
mockFn.mockReturnValue('result');
// Return different values on subsequent calls
mockFn
.mockReturnValueOnce('first')
.mockReturnValueOnce('second')
.mockReturnValue('default');
// Return resolved promise
mockFn.mockResolvedValue({ data: 'value' });
// Return rejected promise
mockFn.mockRejectedValue(new Error('Failed'));
Mock Implementation
const mockFn = vi.fn().mockImplementation((a: number, b: number) => a + b);
expect(mockFn(1, 2)).toBe(3);
// One-time implementation
mockFn.mockImplementationOnce(() => 'special');
vi.spyOn() - Method Spies
Spy on Object Methods
import { vi, describe, it, expect, afterEach } from 'vitest';
const calculator = {
add(a: number, b: number): number {
return a + b;
},
};
describe('calculator', () => {
afterEach(() => {
vi.restoreAllMocks();
});
it('should spy on add method', () => {
const spy = vi.spyOn(calculator, 'add');
calculator.add(1, 2);
expect(spy).toHaveBeenCalledWith(1, 2);
expect(spy).toHaveReturnedWith(3);
});
it('should mock return value', () => {
vi.spyOn(calculator, 'add').mockReturnValue(100);
expect(calculator.add(1, 2)).toBe(100);
});
});
Spy on Class Methods
class UserService {
async getUser(id: string): Promise<User> {
// Real implementation
}
}
it('should spy on class method', async () => {
const service = new UserService();
const spy = vi.spyOn(service, 'getUser').mockResolvedValue({
id: '123',
name: 'Test User',
});
const user = await service.getUser('123');
expect(spy).toHaveBeenCalledWith('123');
expect(user.name).toBe('Test User');
});
vi.mock() - Module Mocking
Auto-Mock Module
import { vi, describe, it, expect } from 'vitest';
import { fetchUser } from './api';
// Mock entire module
vi.mock('./api');
describe('with mocked api', () => {
it('should use mocked function', async () => {
// fetchUser is now a mock
vi.mocked(fetchUser).mockResolvedValue({ id: '1', name: 'Mock User' });
const user = await fetchUser('1');
expect(user.name).toBe('Mock User');
});
});
Manual Mock Implementation
vi.mock('./api', () => ({
fetchUser: vi.fn().mockResolvedValue({ id: '1', name: 'Mock User' }),
fetchPosts: vi.fn().mockResolvedValue([]),
}));
Partial Mock
vi.mock('./utils', async (importOriginal) => {
const actual = await importOriginal<typeof import('./utils')>();
return {
...actual,
// Only mock specific exports
formatDate: vi.fn().mockReturnValue('mocked date'),
};
});
Mocking External Modules
Mock Node.js Modules
import { vi } from 'vitest';
import fs from 'node:fs';
vi.mock('node:fs');
it('should mock fs.readFile', async () => {
vi.mocked(fs.readFile).mockImplementation((path, callback) => {
callback(null, 'mocked content');
});
});
Mock npm Packages
vi.mock('axios', () => ({
default: {
get: vi.fn().mockResolvedValue({ data: { id: 1 } }),
post: vi.fn().mockResolvedValue({ data: { success: true } }),
},
}));
Timer Mocks
Mock setTimeout/setInterval
import { vi, describe, it, expect, beforeEach, afterEach } from 'vitest';
describe('timer tests', () => {
beforeEach(() => {
vi.useFakeTimers();
});
afterEach(() => {
vi.useRealTimers();
});
it('should advance timers', () => {
const callback = vi.fn();
setTimeout(callback, 1000);
expect(callback).not.toHaveBeenCalled();
vi.advanceTimersByTime(1000);
expect(callback).toHaveBeenCalled();
});
it('should run all timers', () => {
const callback = vi.fn();
setTimeout(callback, 1000);
setTimeout(callback, 2000);
vi.runAllTimers();
expect(callback).toHaveBeenCalledTimes(2);
});
});
Mock Date
beforeEach(() => {
vi.useFakeTimers();
vi.setSystemTime(new Date('2024-01-01'));
});
afterEach(() => {
vi.useRealTimers();
});
it('should use mocked date', () => {
expect(new Date().toISOString()).toBe('2024-01-01T00:00:00.000Z');
});
Mock Assertions
const mockFn = vi.fn();
// Call count
expect(mockFn).toHaveBeenCalledTimes(3);
expect(mockFn).toHaveBeenCalledOnce();
// Call arguments
expect(mockFn).toHaveBeenCalledWith('arg1', 'arg2');
expect(mockFn).toHaveBeenLastCalledWith('last', 'args');
expect(mockFn).toHaveBeenNthCalledWith(2, 'second', 'call');
// Return values
expect(mockFn).toHaveReturned();
expect(mockFn).toHaveReturnedWith('value');
expect(mockFn).toHaveLastReturnedWith('last');
// Access call info
expect(mockFn.mock.calls[0]).toEqual(['first', 'call']);
expect(mockFn.mock.results[0].value).toBe('result');
Reset and Restore
const mockFn = vi.fn();
// Clear call history (keeps implementation)
mockFn.mockClear();
// or
vi.clearAllMocks();
// Reset to initial state (clears implementation)
mockFn.mockReset();
// or
vi.resetAllMocks();
// Restore original implementation (for spies)
mockFn.mockRestore();
// or
vi.restoreAllMocks();
Test Doubles Patterns
Stub - Provides Canned Answers
const userRepository = {
findById: vi.fn().mockResolvedValue({ id: '1', name: 'Test' }),
};
Spy - Records Calls
const analytics = {
track: vi.fn(),
};
// Act
performAction();
// Assert calls were made
expect(analytics.track).toHaveBeenCalledWith('action', { type: 'click' });
Fake - Working Implementation
class FakeUserRepository implements UserRepository {
private users = new Map<string, User>();
async save(user: User): Promise<void> {
this.users.set(user.id, user);
}
async findById(id: string): Promise<User | null> {
return this.users.get(id) ?? null;
}
}
Best Practices Summary
- Mock at boundaries - APIs, databases, file system
- Don't mock what you own - Test your own code directly
- Clear mocks between tests - Use beforeEach/afterEach
- Use vi.mocked() for type safety - TypeScript-aware mocking
- Prefer spies over mocks - When you need real behavior
- Keep mocks simple - Complex mocks indicate design issues
- Restore after tests - Prevent test pollution
Code Review Checklist
- Mocks cleared between tests
- Only external dependencies mocked
- vi.mocked() used for type safety
- Spies restored after use
- Mock assertions verify behavior
- No unnecessary mocking
- Fake implementations for complex scenarios