Hone logo
Hone
Problems

Jest Mock Factory for Service Dependencies

In modern software development, it's crucial to write unit tests that isolate the code under test. This often involves mocking external dependencies, such as services or API clients, to ensure your tests focus solely on the logic of the component being tested. This challenge will guide you in creating a flexible mock factory for your Jest tests.

Problem Description

You are tasked with creating a reusable "mock factory" function using Jest. This factory should simplify the process of creating mock objects for various dependencies within your TypeScript codebase. The factory should allow you to:

  • Define default mock implementations: Provide a base set of mock functions for common methods.
  • Override specific methods: Allow customization of individual mock function implementations on a per-instance basis.
  • Handle different dependency shapes: Be flexible enough to mock objects with varying sets of methods.

This factory is useful for reducing boilerplate code in your Jest tests, making them more readable and maintainable.

Examples

Example 1: Mocking a simple UserService

Let's say you have a UserService interface:

interface UserService {
  getUser(id: string): Promise<{ id: string; name: string }>;
  createUser(name: string): Promise<{ id: string; name: string }>;
}

Your mock factory should be able to generate a UserService mock.

Input:

A call to your mock factory with a default implementation for getUser and createUser.

// Assume you have a factory function `createMockFactory`
const userServiceMockFactory = createMockFactory<UserService>({
  getUser: jest.fn(async (id) => ({ id, name: `User ${id}` })),
  createUser: jest.fn(async (name) => ({ id: 'new-user-id', name })),
});

const mockUserService = userServiceMockFactory();

Output:

A mock object that conforms to the UserService interface, with its methods mocked using jest.fn().

// The `mockUserService` object would look something like this (internally):
{
  getUser: jest.fn().mockImplementation(async (id) => ({ id, name: `User ${id}` })),
  createUser: jest.fn().mockImplementation(async (name) => ({ id: 'new-user-id', name })),
}

Example 2: Overriding a method during mock creation

Input:

Creating a UserService mock but wanting to change the behavior of getUser for a specific test scenario.

const userServiceMockFactory = createMockFactory<UserService>({
  getUser: jest.fn(async (id) => ({ id, name: `User ${id}` })),
  createUser: jest.fn(async (name) => ({ id: 'new-user-id', name })),
});

const specificUserMock = userServiceMockFactory({
  getUser: jest.fn(async (id) => ({ id, name: 'Specific User' })),
});

Output:

A mock object where getUser has the overridden implementation, and createUser uses the default.

// The `specificUserMock` object would look something like this (internally):
{
  getUser: jest.fn().mockImplementation(async (id) => ({ id, name: 'Specific User' })),
  createUser: jest.fn().mockImplementation(async (name) => ({ id: 'new-user-id', name })),
}

Example 3: Mocking a dependency with optional methods

Consider an interface with optional methods:

interface AnalyticsService {
  trackEvent?(eventName: string, eventData?: Record<string, any>): void;
  identify?(userId: string, traits?: Record<string, any>): void;
}

Input:

Creating a mock for AnalyticsService.

const analyticsMockFactory = createMockFactory<AnalyticsService>({}); // No defaults

const mockAnalytics = analyticsMockFactory();

Output:

An object where trackEvent and identify are mock functions, even though they were optional and no defaults were provided.

// The `mockAnalytics` object would look something like this (internally):
{
  trackEvent: jest.fn(),
  identify: jest.fn(),
}

Constraints

  • The mock factory should accept a generic type T representing the interface of the dependency being mocked.
  • The factory should return a function that, when called, returns a mock object of type T.
  • The returned mock object's methods should all be jest.fn() instances.
  • The factory should allow for default mock implementations to be provided during its creation.
  • The factory's returned function should allow for specific method implementations to be overridden when creating a mock instance.
  • If a method is optional in the interface (e.g., method?: () => void), it should still be mocked even if no default or override is provided.

Notes

  • You will need to leverage TypeScript's advanced types, specifically mapped types and possibly Partial, to achieve this.
  • Consider how to handle methods that return promises when defining default or overridden implementations. Jest's jest.fn() can be used with .mockResolvedValue() or .mockRejectedValue() for promise-based functions.
  • The goal is to create a highly reusable and type-safe utility for your Jest test setup. Think about how to ensure the returned mock object strictly adheres to the input interface T.
Loading editor...
typescript