Claude Code Jest Mock Modules and Spies Deep Dive Guide
Effective unit testing requires isolating code under test from its dependencies. Jest provides powerful mocking capabilities that enable you to replace real implementations with controlled mocks, spies, and stubs. This deep dive guide explores advanced mocking techniques with Jest, demonstrating how Claude Code can help you write comprehensive tests that verify behavior rather than implementation details.
Understanding Mock Functions and Their Applications
Mock functions (also called spies) are the foundation of Jest’s testing capabilities. They allow you to capture function calls, modify return values, and verify interactions without affecting the actual implementation. When working with Claude Code, you can describe your testing intent and receive guidance on selecting the appropriate mocking strategy for your specific scenario.
Creating Basic Mocks
The simplest way to create a mock function is through jest.fn(). This creates a mock implementation that tracks all calls and arguments:
// Creating a basic mock function
const mockCallback = jest.fn((x) => x * 2);
mockCallback(5); // Returns 10
mockCallback(3); // Returns 6
// Verify the function was called
expect(mockCallback).toHaveBeenCalled();
// Verify number of calls
expect(mockCallback).toHaveBeenCalledTimes(2);
// Verify specific arguments
expect(mockCallback).toHaveBeenCalledWith(5);
expect(mockCallback).toHaveBeenLastCalledWith(3);
Claude Code can help you generate these mocks dynamically based on the functions you’re testing. Simply describe the interface you need, and it will construct appropriate mock configurations.
Mocking Modules with jest.mock()
When your code imports external modules, you often need to replace the entire module with a mock version. Jest’s jest.mock() provides module-level mocking that applies to all imports within a test file.
Complete Module Replacement
Consider a service that makes API calls:
// src/services/userService.ts
import { apiClient } from './apiClient';
import { Logger } from './logger';
export async function getUserById(userId: string) {
try {
const response = await apiClient.get(`/users/${userId}`);
Logger.info(`Fetched user ${userId}`);
return response.data;
} catch (error) {
Logger.error(`Failed to fetch user ${userId}`, error);
throw error;
}
}
To test this without making real API calls, mock the dependencies:
// src/services/__tests__/userService.test.ts
import { getUserById } from '../userService';
// Mock the entire module
jest.mock('../apiClient', () => ({
apiClient: {
get: jest.fn(),
},
}));
jest.mock('../logger', () => ({
Logger: {
info: jest.fn(),
error: jest.fn(),
},
}));
import { apiClient } from '../apiClient';
import { Logger } from '../logger';
describe('getUserById', () => {
beforeEach(() => {
jest.clearAllMocks();
});
it('fetches user and logs info on success', async () => {
const mockUser = { id: '123', name: 'John Doe' };
apiClient.get.mockResolvedValue({ data: mockUser });
const result = await getUserById('123');
expect(result).toEqual(mockUser);
expect(apiClient.get).toHaveBeenCalledWith('/users/123');
expect(Logger.info).toHaveBeenCalledWith('Fetched user 123');
});
it('logs error and rethrows on failure', async () => {
const error = new Error('Network error');
apiClient.get.mockRejectedValue(error);
await expect(getUserById('123')).rejects.toThrow('Network error');
expect(Logger.error).toHaveBeenCalledWith('Failed to fetch user 123', error);
});
});
Partial Module Mocking with jest.spyOn()
Sometimes you only need to mock specific methods of an object while preserving others. jest.spyOn() creates a mock that wraps an existing method, allowing you to intercept calls while keeping the original implementation available:
describe('Partial Module Mocking', () => {
it('spies on console.log without replacing it', () => {
const consoleSpy = jest.spyOn(console, 'log');
// Call the function that uses console.log
greetUser('Alice');
expect(consoleSpy).toHaveBeenCalledWith('Hello, Alice!');
// console.error still works normally
consoleSpy.mockRestore();
});
});
Advanced Spy Techniques for Complex Scenarios
Mocking Object Methods
For objects with multiple methods, jest.spyOn() provides fine-grained control:
const mathUtils = {
add: (a, b) => a + b,
subtract: (a, b) => a - b,
multiply: (a, b) => a * b,
};
describe('Math Operations', () => {
it('can spy on individual methods', () => {
const addSpy = jest.spyOn(mathUtils, 'add');
const result = mathUtils.add(2, 3);
expect(result).toBe(5);
expect(addSpy).toHaveBeenCalledWith(2, 3);
addSpy.mockRestore();
});
it('can mock implementation temporarily', () => {
const addSpy = jest.spyOn(mathUtils, 'add').mockImplementation((a, b) => 100);
expect(mathUtils.add(2, 3)).toBe(100);
expect(mathUtils.subtract(2, 3)).toBe(-1); // Original still works
addSpy.mockRestore();
expect(mathUtils.add(2, 3)).toBe(5); // Restored
});
});
Chaining Mock Methods
Jest spies support method chaining for complex scenarios:
it('demonstrates spy method chaining', () => {
const apiCall = jest.fn().mockReturnValue('first call');
expect(apiCall()).toBe('first call');
// Change return value for subsequent calls
apiCall.mockReturnValue('second call').mockImplementationOnce(() => 'one-time');
expect(apiCall()).toBe('one-time'); // Uses implementationOnce
expect(apiCall()).toBe('second call'); // Uses mockReturnValue
});
Working with Async Mocks and Promises
Asynchronous code requires special attention when mocking. Jest provides several patterns for handling promises and async operations:
describe('Async Mocking Patterns', () => {
it('mocks async functions with mockResolvedValue', async () => {
const fetchData = jest.fn().mockResolvedValue({ name: 'Test' });
const result = await fetchData();
expect(result).toEqual({ name: 'Test' });
});
it('mocks rejected promises', async () => {
const fetchData = jest.fn().mockRejectedValue(new Error('Failed'));
await expect(fetchData()).rejects.toThrow('Failed');
});
it('handles chained promises', async () => {
const api = {
getUser: jest.fn().mockResolvedValue({ id: 1 }),
getPosts: jest.fn().mockResolvedValue([{ title: 'Post 1' }]),
};
const user = await api.getUser(1);
const posts = await api.getPosts(user.id);
expect(posts).toHaveLength(1);
expect(api.getUser).toHaveBeenCalledWith(1);
});
});
Best Practices for Effective Mocking
1. Clean Up After Tests
Always restore or clear mocks to prevent test pollution:
describe('Cleanup Best Practices', () => {
let spy;
beforeEach(() => {
spy = jest.spyOn(Math, 'random');
});
afterEach(() => {
spy.mockRestore(); // Important!
});
it('tests with mocked random', () => {
spy.mockReturnValue(0.5);
expect(Math.random()).toBe(0.5);
});
});
2. Use Descriptive Mock Names
When working with Claude Code, describe your mocks clearly:
// Instead of vague mocks:
const mock = jest.fn();
// Describe the intent:
const mockFetchUserById = jest.fn();
const mockLogger = jest.fn();
3. Mock at the Right Level
Choose the appropriate mocking strategy based on what you’re testing:
- Unit tests: Mock external dependencies (API calls, databases)
- Integration tests: Mock only infrastructure (HTTP clients)
- E2E tests: Use real implementations where possible
Conclusion
Mastering Jest’s mocking capabilities transforms your tests from simple assertions into powerful verification tools. Mock functions and spies enable you to test complex interactions, verify behavior, and isolate code under test effectively. With Claude Code’s assistance, you can quickly generate appropriate mocks, debug failing tests, and explore advanced patterns tailored to your specific testing challenges.
Remember to keep mocks focused, clean up after tests, and choose the right mocking strategy for your testing context. These practices will lead to more maintainable, reliable test suites that give you confidence in your code’s behavior.
Related Reading
- Claude Code for Beginners: Complete Getting Started Guide
- Best Claude Skills for Developers in 2026
- Claude Skills Guides Hub
Built by theluckystrike — More at zovo.one