Hone logo
Hone
Problems

Jest Mock Reset Challenge

In unit testing, it's crucial to ensure that mocks are clean for each test to prevent unintended state leakage between tests. Jest provides powerful tools for mocking, and understanding how to reset mocks between test cases is a fundamental skill. This challenge will test your ability to effectively manage mock state using Jest's jest.clearAllMocks() and jest.resetAllMocks().

Problem Description

You are tasked with writing Jest tests for a simple UserService that has a dependency on an external ApiServiceClient. The ApiServiceClient's methods are mocked. Your goal is to demonstrate the correct usage of jest.clearAllMocks() and jest.resetAllMocks() to ensure that mock call counts and implementations are reset between different test cases.

Requirements:

  1. Mock ApiServiceClient: Create a mock for the ApiServiceClient class and its methods (getUser and createUser).
  2. Test UserService Methods: Implement tests for UserService's fetchUser and addUser methods, which internally call the mocked ApiServiceClient.
  3. Demonstrate Mock Reset:
    • In one set of tests, use jest.clearAllMocks() to reset only the call history (invocation counts and arguments) of all mocks. The mock implementations should remain.
    • In another set of tests, use jest.resetAllMocks() to reset both the call history and the mock implementations (resetting them to their default state, often jest.fn()).
  4. Assertions: Assert that mock functions are called the correct number of times and with the correct arguments after each operation.

Expected Behavior:

  • Tests using jest.clearAllMocks() should see mock implementations persist across tests, but call counts should reset.
  • Tests using jest.resetAllMocks() should see both call counts and mock implementations reset to a fresh state.

Edge Cases:

  • Consider a scenario where a mock implementation is explicitly defined. How does clearAllMocks and resetAllMocks affect it?

Examples

Let's assume the following simplified UserService and ApiServiceClient structure:

// apiService.ts
export class ApiServiceClient {
  async getUser(id: string): Promise<{ id: string; name: string }> {
    throw new Error("Not implemented");
  }

  async createUser(name: string): Promise<{ id: string; name: string }> {
    throw new Error("Not implemented");
  }
}

// userService.ts
import { ApiServiceClient } from "./apiService";

export class UserService {
  constructor(private apiService: ApiServiceClient) {}

  async fetchUser(id: string): Promise<{ id: string; name: string }> {
    return this.apiService.getUser(id);
  }

  async addUser(name: string): Promise<{ id: string; name: string }> {
    return this.apiService.createUser(name);
  }
}

Example Scenario for Testing:

We will write tests for userService.ts.

Test Case Setup (Illustrative - actual tests will be in your code):

Imagine a test file.

// userService.test.ts

// Mock the ApiServiceClient
const mockApiService = {
  getUser: jest.fn(),
  createUser: jest.fn(),
};

// Instantiate UserService with the mock
const userService = new UserService(mockApiService as any); // Using 'as any' for simplicity in example

describe('UserService with Jest Mock Reset', () => {

  // --- Tests using jest.clearAllMocks() ---
  describe('using jest.clearAllMocks()', () => {
    // Setup for this describe block: clear mocks before each test
    beforeEach(() => {
      jest.clearAllMocks();
      // Optionally, re-implement mocks if needed after clearing
      mockApiService.getUser.mockResolvedValue({ id: '123', name: 'Alice' });
      mockApiService.createUser.mockResolvedValue({ id: '456', name: 'Bob' });
    });

    test('fetchUser should call ApiServiceClient.getUser once', async () => {
      await userService.fetchUser('123');
      expect(mockApiService.getUser).toHaveBeenCalledTimes(1);
      expect(mockApiService.getUser).toHaveBeenCalledWith('123');
    });

    test('addUser should call ApiServiceClient.createUser once', async () => {
      await userService.addUser('Bob');
      expect(mockApiService.createUser).toHaveBeenCalledTimes(1);
      expect(mockApiService.createUser).toHaveBeenCalledWith('Bob');
    });

    test('fetchUser called again should increment call count', async () => {
      await userService.fetchUser('123'); // First call
      await userService.fetchUser('123'); // Second call
      expect(mockApiService.getUser).toHaveBeenCalledTimes(2);
    });
  });

  // --- Tests using jest.resetAllMocks() ---
  describe('using jest.resetAllMocks()', () => {
    // Setup for this describe block: reset mocks before each test
    beforeEach(() => {
      jest.resetAllMocks();
      // Re-implement mocks after reset as resetAllMocks clears implementations too
      mockApiService.getUser.mockResolvedValue({ id: '789', name: 'Charlie' });
      mockApiService.createUser.mockResolvedValue({ id: '101', name: 'David' });
    });

    test('fetchUser should call ApiServiceClient.getUser once after reset', async () => {
      await userService.fetchUser('789');
      expect(mockApiService.getUser).toHaveBeenCalledTimes(1);
      expect(mockApiService.getUser).toHaveBeenCalledWith('789');
    });

    test('addUser should call ApiServiceClient.createUser once after reset', async () => {
      await userService.addUser('David');
      expect(mockApiService.createUser).toHaveBeenCalledTimes(1);
      expect(mockApiService.createUser).toHaveBeenCalledWith('David');
    });

    test('fetchUser called again should reset call count to 1 after reset', async () => {
      await userService.fetchUser('789'); // First call
      // If resetAllMocks was not in beforeEach, this would be 2.
      // But with resetAllMocks before each test, the count resets.
      // This test is more about proving the *reset* rather than accumulation within this block.
      // A better demonstration here would be to show the mock *implementation* is reset.
      expect(mockApiService.getUser).toHaveBeenCalledTimes(1);

      // To demonstrate resetAllMocks clearing implementation:
      // Let's imagine a previous test in this block *changed* the mock implementation.
      // If we were to call fetchUser again WITHOUT a beforeEach reset,
      // the implementation would be the *original* one.
      // With resetAllMocks, it reverts.

      // A more direct test of implementation reset:
      // Assume a test before this one within this 'resetAllMocks' block modified the mock.
      // For example:
      // mockApiService.getUser.mockImplementation(() => Promise.reject(new Error("Error occurred")));
      // await userService.fetchUser('bad-id'); // This would now reject
      // ... assertions for rejection ...
      // After resetAllMocks(), a *new* call to fetchUser('789') should use the new mockResolvedValue.
    });
  });
});

Explanation:

  • jest.clearAllMocks(): Resets the .calls, .instances, .results properties of all mocks, but leaves mock implementations (like .mockResolvedValue(), .mockImplementation()) intact.
  • jest.resetAllMocks(): Resets everything that jest.clearAllMocks() does, and also restores mock implementations to their default state (equivalent to calling .mockClear() and then setting them to jest.fn()).

Constraints

  • Your solution must use TypeScript.
  • You must use Jest for testing.
  • All assertions must be made against the mock functions of ApiServiceClient.
  • Do not implement the actual ApiServiceClient or UserService logic beyond what's necessary for mocking and testing. Focus on the mock reset mechanisms.

Notes

  • Consider the scope of your beforeEach or afterEach hooks. Where you place jest.clearAllMocks() or jest.resetAllMocks() is crucial for how mocks behave across tests.
  • Think about how explicit mock implementations (.mockResolvedValue, .mockImplementation) interact with both reset functions.
  • The goal is to understand the difference and appropriate use cases for clearAllMocks versus resetAllMocks.
Loading editor...
typescript