Hone logo
Hone
Problems

Mocking External Dependencies with jest.spyOn

This challenge focuses on mastering jest.spyOn, a powerful Jest utility for creating spies on methods of existing objects. You'll learn how to intercept function calls, assert their behavior, and even mock their return values or implementations. This is crucial for isolating the code you're testing from its dependencies, making your tests more reliable and maintainable.

Problem Description

Your task is to implement a series of tests for a hypothetical UserService class. This UserService class relies on an external DatabaseService for data persistence. To effectively test UserService in isolation, you need to mock the methods of DatabaseService using jest.spyOn.

What needs to be achieved:

  1. Mocking a method: Create a spy on a specific method of the DatabaseService to verify it's called.
  2. Mocking with a specific return value: Make the mocked method return a predefined value.
  3. Mocking with a custom implementation: Provide a custom function to replace the original method's logic.
  4. Restoring spies: Ensure that after tests are run, the original methods are restored.

Key Requirements:

  • You will be provided with stubbed DatabaseService and UserService classes.
  • Your solution will be within the UserService.test.ts file.
  • Use jest.spyOn to mock methods of DatabaseService.
  • Use Jest's assertion methods (e.g., toHaveBeenCalled, toHaveReturnedWith, toHaveBeenCalledWith).

Expected Behavior:

  • Tests should accurately reflect the interaction between UserService and DatabaseService.
  • Spies should be correctly set up and cleaned up.

Edge Cases to Consider:

  • What happens if the DatabaseService method is not found? (Though for this challenge, we assume it exists).
  • Ensuring spies are restored to avoid test pollution.

Examples

Let's assume the following (simplified) class definitions:

// databaseService.ts
export class DatabaseService {
  getUserById(id: number): Promise<any | null> {
    // Actual database interaction logic would be here
    console.log(`Fetching user with ID: ${id} from DB`);
    return Promise.resolve({ id, name: "John Doe", email: "john.doe@example.com" });
  }

  saveUser(user: any): Promise<void> {
    // Actual database save logic would be here
    console.log(`Saving user: ${user.name}`);
    return Promise.resolve();
  }
}

// userService.ts
import { DatabaseService } from "./databaseService";

export class UserService {
  private dbService: DatabaseService;

  constructor(dbService: DatabaseService) {
    this.dbService = dbService;
  }

  async getUserProfile(userId: number): Promise<string | null> {
    const user = await this.dbService.getUserById(userId);
    if (user) {
      return `User Profile: ${user.name} (${user.email})`;
    }
    return null;
  }

  async createUser(userData: { name: string; email: string }): Promise<void> {
    // Simulate some validation or processing
    if (!userData.name || !userData.email) {
      throw new Error("Name and email are required.");
    }
    await this.dbService.saveUser(userData);
  }
}

Example 1: Verifying a method call

Imagine you want to test that UserService.getUserProfile correctly calls DatabaseService.getUserById.

  • Input: userService.getUserProfile(1)
  • Expected Behavior: DatabaseService.getUserById should be called once with the argument 1.

Example 2: Mocking a return value

Now, let's test UserService.getUserProfile when DatabaseService.getUserById returns a specific user.

  • Input: userService.getUserProfile(2)
  • Mocked DatabaseService.getUserById(2): Returns Promise.resolve({ id: 2, name: "Jane Smith", email: "jane.smith@example.com" })
  • Output: "User Profile: Jane Smith (jane.smith@example.com)"

Example 3: Mocking with a custom implementation

Test UserService.createUser and mock DatabaseService.saveUser to throw an error, simulating a database failure.

  • Input: userService.createUser({ name: "Alice", email: "alice@example.com" })
  • Mocked DatabaseService.saveUser: Throws a new Error("Database connection failed").
  • Expected Behavior: The createUser method should reject with the error "Database connection failed".

Constraints

  • All tests must be written in TypeScript.
  • You must use jest.spyOn for mocking.
  • Each test case should be self-contained and not rely on side effects from other tests.
  • The DatabaseService and UserService classes are provided and should not be modified.
  • Use jest.fn() to create mock functions when needed for mockImplementation or mockReturnValue.

Notes

  • Remember to import your services into the test file.
  • Consider using beforeEach and afterEach to set up and tear down your spies to ensure a clean test environment for each test.
  • jest.spyOn returns a Jest mock function, allowing you to use methods like mockReturnValue, mockImplementation, toHaveBeenCalled, etc.
  • Pay close attention to asynchronous operations (Promises) and use await appropriately in your tests.
  • The dbService instance passed to UserService in the tests should be an instance of the real DatabaseService so that spyOn can attach to its methods.
Loading editor...
typescript