Claude Skills Guide

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:

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.

Built by theluckystrike — More at zovo.one