Hone logo
Hone
Problems

Mocking External Dependencies with Jest

Testing complex applications often involves external dependencies, such as API clients, database connections, or third-party libraries. Directly interacting with these dependencies during tests can lead to slow, unstable, and costly tests. Jest provides powerful mocking capabilities, particularly jest.mock(), to isolate the code under test and control the behavior of its dependencies. This challenge will help you master jest.mock() for robust and efficient unit testing.

Problem Description

Your task is to implement tests for a TypeScript module that relies on an external service for fetching user data. You will need to use jest.mock() to mock this external service and simulate different scenarios, such as successful data retrieval and error conditions.

What needs to be achieved:

  1. Create a module that depends on an external function to fetch user data.
  2. Write unit tests for this module using Jest.
  3. Utilize jest.mock() to mock the external data fetching function.
  4. Assert that your module behaves correctly under different mocked responses from the external service.

Key requirements:

  • The external data fetching function should be defined in a separate file (e.g., userService.ts).
  • Your module under test (e.g., userProfile.ts) should import and use this external function.
  • Tests should be written in a separate file (e.g., userProfile.test.ts).
  • Use jest.mock() to mock the userService module.
  • Implement tests for both successful data retrieval and error cases.
  • When mocking, ensure you can control the return value and/or the thrown error of the mocked function.

Expected behavior:

  • When the external service successfully returns user data, your module should process and return that data as expected.
  • When the external service throws an error, your module should handle the error gracefully (e.g., by returning null or throwing a specific error).

Edge cases to consider:

  • What happens if the external service returns an empty object or null when a user is not found? Your tests should account for this.

Examples

Let's assume we have the following files:

userService.ts

interface User {
    id: number;
    name: string;
    email: string;
}

export const fetchUserById = async (userId: number): Promise<User | null> => {
    // In a real scenario, this would make an actual API call.
    // For this challenge, it's the function we will mock.
    console.log(`Fetching user ${userId} from real service...`);
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            if (userId === 1) {
                resolve({ id: 1, name: "Alice", email: "alice@example.com" });
            } else if (userId === 2) {
                resolve({ id: 2, name: "Bob", email: "bob@example.com" });
            } else {
                resolve(null); // User not found
            }
        }, 100);
    });
};

export const fetchUserByIdWithError = async (userId: number): Promise<User> => {
    // Another function to simulate errors
    console.log(`Attempting to fetch user ${userId} with potential error...`);
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            if (userId === 3) {
                resolve({ id: 3, name: "Charlie", email: "charlie@example.com" });
            } else {
                reject(new Error("Network error fetching user"));
            }
        }, 100);
    });
};

userProfile.ts

import { fetchUserById, fetchUserByIdWithError } from './userService';

interface User {
    id: number;
    name: string;
    email: string;
}

export const getUserProfile = async (userId: number): Promise<User | null> => {
    try {
        const user = await fetchUserById(userId);
        return user;
    } catch (error) {
        console.error("Error fetching user profile:", error);
        return null;
    }
};

export const getUserProfileWithErrorHandling = async (userId: number): Promise<User | null> => {
    try {
        const user = await fetchUserByIdWithError(userId);
        return user;
    } catch (error) {
        console.error("Error fetching user profile:", error.message);
        return null;
    }
};

Example 1: Successful User Fetch

Input (for userProfile.test.ts): Simulate fetchUserById to return a specific user for userId = 1.

Expected Output (from running getUserProfile(1) in the test):

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

Explanation: The test will mock userService.fetchUserById to return a predefined user object when userId is 1. The getUserProfile function will then return this mocked user object.

Example 2: User Not Found

Input (for userProfile.test.ts): Simulate fetchUserById to return null for userId = 99.

Expected Output (from running getUserProfile(99) in the test):

null

Explanation: The test will mock userService.fetchUserById to return null. The getUserProfile function will correctly return null as it receives null from the mocked dependency.

Example 3: Error Handling

Input (for userProfile.test.ts): Simulate fetchUserByIdWithError to throw an error when called with userId = 4.

Expected Output (from running getUserProfileWithErrorHandling(4) in the test):

null

Explanation: The test will mock userService.fetchUserByIdWithError to throw an Error. The getUserProfileWithErrorHandling function's try...catch block will catch this error, log it, and return null.

Constraints

  • All code must be written in TypeScript.
  • You must use jest.mock() to mock the userService module.
  • Your tests should cover at least one successful scenario, one "not found" scenario, and one error scenario.
  • Do not modify the userService.ts or userProfile.ts files directly for testing purposes. The mocking should be done within the test file.

Notes

  • Remember to import the functions you are testing.
  • When mocking a module, you can control the behavior of its exported functions. For asynchronous functions (like async/await), you'll need to return Promises that resolve or reject as needed.
  • Consider how you will tell Jest which file to mock.
  • Pay attention to the jest.fn() utility for creating mock functions and configuring their return values or implementations.
  • Use await appropriately when testing asynchronous functions.
Loading editor...
typescript