Hone logo
Hone
Problems

Mocking Modules in Jest

When developing applications, it's common to have modules that depend on external services or complex internal logic. To effectively test a specific unit of code without being affected by these dependencies, we need to isolate it. Jest provides powerful mocking capabilities, allowing us to replace entire modules with controlled fakes during testing. This challenge will guide you through creating and using mock modules in Jest with TypeScript.

Problem Description

Your task is to create a test for a UserService class that depends on an external HttpClient module. The HttpClient module is responsible for making network requests, but for testing purposes, we want to control its responses without actually hitting a network. You will need to:

  1. Create a mock version of the HttpClient module. This mock should allow you to define specific return values for its methods (e.g., get, post).
  2. Configure Jest to use your mock HttpClient module when testing the UserService.
  3. Write tests for the UserService that leverage the mocked HttpClient to verify its behavior under different scenarios.

This is a crucial skill for writing robust and maintainable unit tests, as it ensures your tests are fast, deterministic, and focused on the logic you intend to test.

Examples

Let's assume we have the following conceptual code (you'll need to implement these in your solution):

httpClient.ts (The module to be mocked):

// This is a simplified representation of what might exist in a real scenario.
// For this challenge, you don't need to implement the actual network logic.
export class HttpClient {
  async get(url: string): Promise<any> {
    console.log(`Making actual GET request to: ${url}`);
    // In a real scenario, this would make an HTTP request
    throw new Error("Not implemented for challenge");
  }

  async post(url: string, data: any): Promise<any> {
    console.log(`Making actual POST request to: ${url} with data:`, data);
    // In a real scenario, this would make an HTTP request
    throw new Error("Not implemented for challenge");
  }
}

userService.ts (The module to be tested):

import { HttpClient } from './httpClient';

export class UserService {
  private httpClient: HttpClient;

  constructor(httpClient: HttpClient) {
    this.httpClient = httpClient;
  }

  async getUserById(userId: string): Promise<{ id: string; name: string; email: string }> {
    const response = await this.httpClient.get(`/users/${userId}`);
    // Assuming the response is directly the user object
    return response;
  }

  async createUser(name: string, email: string): Promise<{ id: string; name: string; email: string }> {
    const newUser = await this.httpClient.post('/users', { name, email });
    return newUser;
  }
}

Example 1: Testing getUserById for a successful fetch

Input:

  • userId passed to getUserById: "123"
  • Mocked HttpClient.get('/users/123') to return: { id: "123", name: "Alice", email: "alice@example.com" }

Output of userService.getUserById("123"):

{
  "id": "123",
  "name": "Alice",
  "email": "alice@example.com"
}

Explanation: The userService.getUserById method calls this.httpClient.get('/users/123'). Because we've mocked HttpClient.get, it returns the predefined user object, which is then returned by getUserById.

Example 2: Testing createUser with successful creation

Input:

  • name passed to createUser: "Bob"
  • email passed to createUser: "bob@example.com"
  • Mocked HttpClient.post('/users', { name: "Bob", email: "bob@example.com" }) to return: { id: "456", name: "Bob", email: "bob@example.com" }

Output of userService.createUser("Bob", "bob@example.com"):

{
  "id": "456",
  "name": "Bob",
  "email": "bob@example.com"
}

Explanation: The userService.createUser method calls this.httpClient.post('/users', { name, email }). The mock interceptors the POST request and returns a new user object with an ID, which is then returned by createUser.

Example 3: Testing getUserById for a user not found (error handling)

Input:

  • userId passed to getUserById: "999"
  • Mocked HttpClient.get('/users/999') to reject with an error: new Error("User not found")

Expected Behavior: The userService.getUserById("999") call should throw an error.

Explanation: When HttpClient.get rejects with an error, this error propagates through userService.getUserById. In a real application, you might add try...catch blocks in UserService to handle this, but for this challenge, demonstrating the error propagation from the mock is sufficient.

Constraints

  • You must use Jest for testing.
  • All code, including tests and mocks, must be written in TypeScript.
  • The HttpClient class should be imported from a separate file (httpClient.ts) and mocked using jest.mock.
  • The UserService class should instantiate HttpClient within its constructor, allowing for dependency injection.
  • Your mock should allow you to assert that specific httpClient methods were called with the correct arguments.

Notes

  • Consider using jest.mock at the top of your test file to mock the HttpClient module.
  • You'll likely need to use toHaveBeenCalledWith or toHaveReturnedWith matchers to verify interactions with the mock.
  • Think about how you will provide the mocked HttpClient instance to the UserService during testing. This could involve directly mocking the class or using jest.mock in conjunction with mockImplementation.
  • Ensure your mock implementation clearly defines return values for get and post calls.
Loading editor...
typescript