Jest Mock Imported Function in TypeScript

In the world of software development, testing is an integral part of the process. When working with TypeScript projects, Jest has emerged as a popular testing framework. One common scenario in testing is to mock imported functions. Mocking allows us to isolate the unit under test, control the input and output of functions, and make our tests more reliable and faster. This blog will delve into the fundamental concepts, usage methods, common practices, and best practices of mocking imported functions in TypeScript using Jest.

Table of Contents#

  1. Fundamental Concepts
  2. Usage Methods
  3. Common Practices
  4. Best Practices
  5. Conclusion
  6. References

Fundamental Concepts#

What is Mocking?#

Mocking is the process of creating a fake implementation of a function or an object. In the context of testing, mocks are used to replace real dependencies with controlled alternatives. This helps in isolating the unit of code being tested from its external dependencies, such as API calls, database operations, or other functions.

Why Mock Imported Functions?#

  • Isolation: By mocking imported functions, we can test a function in isolation without relying on the actual implementation of the imported function. This makes the test more reliable and less prone to failures due to external factors.
  • Controlled Input and Output: We can control the input and output of the mocked function, which allows us to test different scenarios and edge cases.
  • Faster Tests: Mocking can significantly reduce the execution time of tests, especially when the real function involves time-consuming operations like network requests.

TypeScript and Jest#

TypeScript adds static typing to JavaScript, which helps in catching errors early in the development process. Jest is a JavaScript testing framework that provides built-in support for mocking. When using TypeScript with Jest, we need to ensure that the mocks are correctly typed to avoid type errors.

Usage Methods#

Basic Mocking of an Imported Function#

Let's assume we have a module math.ts with a function add:

// math.ts
export function add(a: number, b: number): number {
  return a + b;
}

And we have another module calculator.ts that imports and uses the add function:

// calculator.ts
import { add } from './math';
 
export function calculateSum(a: number, b: number): number {
  return add(a, b);
}

To test the calculateSum function by mocking the add function, we can use Jest's jest.mock function:

// calculator.test.ts
import { calculateSum } from './calculator';
import { add } from './math';
 
jest.mock('./math');
 
const mockedAdd = add as jest.MockedFunction<typeof add>;
 
describe('calculateSum', () => {
  it('should call add function and return the result', () => {
    const a = 2;
    const b = 3;
    const expectedResult = 5;
 
    mockedAdd.mockReturnValue(expectedResult);
 
    const result = calculateSum(a, b);
 
    expect(mockedAdd).toHaveBeenCalledWith(a, b);
    expect(result).toBe(expectedResult);
  });
});

In this example, we first use jest.mock('./math') to mock the entire math module. Then we cast the add function to a jest.MockedFunction type to ensure type safety. We use mockReturnValue to specify the return value of the mocked function. Finally, we write assertions to verify that the mocked function was called with the correct arguments and that the result of the calculateSum function is as expected.

Mocking with a Custom Implementation#

We can also provide a custom implementation for the mocked function using mockImplementation:

// calculator.test.ts
import { calculateSum } from './calculator';
import { add } from './math';
 
jest.mock('./math');
 
const mockedAdd = add as jest.MockedFunction<typeof add>;
 
describe('calculateSum', () => {
  it('should call add function with custom implementation', () => {
    const a = 2;
    const b = 3;
    const expectedResult = 10;
 
    mockedAdd.mockImplementation((x, y) => x * y);
 
    const result = calculateSum(a, b);
 
    expect(mockedAdd).toHaveBeenCalledWith(a, b);
    expect(result).toBe(expectedResult);
  });
});

In this example, we use mockImplementation to provide a custom implementation for the add function that multiplies the two numbers instead of adding them.

Common Practices#

Mocking External API Calls#

When testing functions that make external API calls, it's common to mock the API calls to avoid making actual network requests. For example, let's assume we have a module api.ts that makes an API call:

// api.ts
import axios from 'axios';
 
export async function fetchData(): Promise<any> {
  const response = await axios.get('https://example.com/api/data');
  return response.data;
}

And we have another module dataProcessor.ts that uses the fetchData function:

// dataProcessor.ts
import { fetchData } from './api';
 
export async function processData(): Promise<any> {
  const data = await fetchData();
  // Process the data
  return data;
}

To test the processData function, we can mock the fetchData function:

// dataProcessor.test.ts
import { processData } from './dataProcessor';
import { fetchData } from './api';
 
jest.mock('./api');
 
const mockedFetchData = fetchData as jest.MockedFunction<typeof fetchData>;
 
describe('processData', () => {
  it('should call fetchData and process the data', async () => {
    const mockData = { message: 'Mocked data' };
 
    mockedFetchData.mockResolvedValue(mockData);
 
    const result = await processData();
 
    expect(mockedFetchData).toHaveBeenCalled();
    expect(result).toEqual(mockData);
  });
});

Mocking Dependencies in a Class#

When testing a class that depends on other functions or modules, we can mock the dependencies to isolate the class under test. For example, let's assume we have a class UserService that depends on a UserRepository:

// userRepository.ts
export interface User {
  id: number;
  name: string;
}
 
export class UserRepository {
  async getUserById(id: number): Promise<User | null> {
    // Fetch the user from the database
    return null;
  }
}
 
// userService.ts
import { UserRepository } from './userRepository';
 
export class UserService {
  constructor(private userRepository: UserRepository) {}
 
  async getUserInfo(id: number): Promise<string | null> {
    const user = await this.userRepository.getUserById(id);
    if (user) {
      return user.name;
    }
    return null;
  }
}

To test the UserService class, we can mock the UserRepository class:

// userService.test.ts
import { UserService } from './userService';
import { UserRepository } from './userRepository';
 
jest.mock('./userRepository');
 
const mockedUserRepository = UserRepository as jest.MockedClass<typeof UserRepository>;
 
describe('UserService', () => {
  it('should get user info', async () => {
    const mockUser = { id: 1, name: 'John Doe' };
    const userService = new UserService(new mockedUserRepository());
 
    const instance = mockedUserRepository.mock.instances[0];
    const mockedGetUserById = instance.getUserById as jest.MockedFunction<typeof instance.getUserById>;
 
    mockedGetUserById.mockResolvedValue(mockUser);
 
    const result = await userService.getUserInfo(1);
 
    expect(mockedGetUserById).toHaveBeenCalledWith(1);
    expect(result).toBe(mockUser.name);
  });
});

Best Practices#

Keep Mocks Simple and Readable#

Mocks should be as simple as possible to make the tests easy to understand and maintain. Avoid creating overly complex mock implementations that can make the tests hard to follow.

Use Descriptive Mock Names#

Use descriptive names for the mocked functions and objects to make the tests more readable. For example, instead of using a generic name like mockFn, use a name that describes the function being mocked, such as mockedAdd or mockedFetchData.

Test Different Scenarios#

When mocking functions, make sure to test different scenarios, such as success cases, error cases, and edge cases. This will help in ensuring that the function under test behaves correctly in all possible situations.

Clean Up Mocks After Each Test#

If you are using mocks in multiple tests, make sure to clean up the mocks after each test to avoid interference between tests. You can use Jest's afterEach function to reset the mocks:

afterEach(() => {
  jest.clearAllMocks();
});

Conclusion#

Mocking imported functions in TypeScript using Jest is a powerful technique that allows us to write more reliable and efficient tests. By understanding the fundamental concepts, usage methods, common practices, and best practices, we can effectively isolate the units under test and control the input and output of the functions. This helps in catching bugs early in the development process and ensuring the quality of the code.

References#