jest.mock() для подмены модулей

Для эффективного тестирования в JavaScript часто необходимо изолировать компоненты и модули, с которыми они взаимодействуют. Одним из способов достижения этой изоляции является подмена зависимостей, и в этом контексте jest.mock() является мощным инструментом. Этот метод позволяет создать заглушки для внешних модулей, чтобы сосредоточиться на тестировании конкретных частей приложения без зависимости от реальной реализации.

Основы работы с jest.mock()

jest.mock() — это функция, предоставляемая Jest для создания подмены для импортируемого модуля. Она позволяет заменить поведение модуля в ходе выполнения тестов на фиктивное или на специально подготовленное.

Когда вызывается jest.mock(), Jest автоматически заменяет импортируемый модуль на объект с пустыми функциями или значениями, которые можно настроить по необходимости.

Пример базового использования:

// math.js
export const sum = (a, b) => a + b;
export const multiply = (a, b) => a * b;

// test.js
import { sum, multiply } from './math';

jest.mock('./math', () => ({
  sum: jest.fn().mockReturnValue(3),
  multiply: jest.fn().mockReturnValue(5)
}));

test('mock sum function', () => {
  expect(sum(1, 2)).toBe(3);  // всегда возвращает 3
});

В этом примере jest.mock() используется для подмены модуля math.js. Функции sum и multiply заменяются на мок-версии, которые всегда возвращают определенные значения, независимо от переданных аргументов. Это позволяет тестировать код, не полагаясь на реальную реализацию этих функций.

Динамическая подмена модулей с использованием jest.mock()

В некоторых случаях требуется изменить поведение мока в разных тестах. Это можно сделать с помощью различных методов, предоставляемых Jest, например, mockReturnValueOnce(), чтобы задать разные возвращаемые значения для каждой конкретной итерации.

jest.mock('./math', () => ({
  sum: jest.fn(),
  multiply: jest.fn()
}));

test('mock sum function with different return values', () => {
  sum.mockReturnValueOnce(3).mockReturnValueOnce(5);
  expect(sum(1, 2)).toBe(3);
  expect(sum(2, 3)).toBe(5);
});

Здесь mockReturnValueOnce() позволяет задать возвращаемое значение для каждой конкретной вызова. Это полезно, когда важно протестировать реакцию кода на различные результаты.

Подмена модулей с использованием модуля jest.mock() и автоматическое восстановление

Jest позволяет автоматически восстанавливать поведение модуля между тестами, чтобы каждый тест начинался с чистого состояния. Для этого существует метод jest.restoreAllMocks(), который можно вызывать после каждого теста или в глобальном блоке.

jest.mock('./math', () => ({
  sum: jest.fn().mockReturnValue(3),
}));

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

test('test mock sum function', () => {
  expect(sum(1, 2)).toBe(3);
  // Модуль восстанавливается перед следующим тестом
});

restoreAllMocks() удаляет все моки, вернув модуль к его изначальному состоянию. Это помогает избежать неожиданных побочных эффектов между тестами.

Мокинг модулей с функциями-constructors

Когда тестируемые модули используют классы или функции-конструкторы, важно помнить, что jest.mock() будет подменять и их. Однако для более сложных случаев, например, когда нужно заменить только методы класса, можно использовать mockImplementationOnce() или mockImplementation().

Пример использования с классом:

// user.js
export class User {
  constructor(name) {
    this.name = name;
  }

  greet() {
    return `Hello, ${this.name}!`;
  }
}

// test.js
import { User } from './user';

jest.mock('./user', () => {
  return {
    User: jest.fn().mockImplementation(() => {
      return { greet: jest.fn().mockReturnValue('Mocked greeting!') };
    })
  };
});

test('test mocked class method', () => {
  const user = new User('Alice');
  expect(user.greet()).toBe('Mocked greeting!');
});

Здесь класс User заменяется на мок, а метод greet() всегда возвращает заданную строку, что позволяет изолировать тестируемый код от реальной реализации.

Мокинг зависимостей в асинхронных тестах

В асинхронных тестах также можно использовать jest.mock() для подмены зависимостей, например, для замены асинхронных запросов на мок-версии. Это позволяет проверить логику приложения, не взаимодействуя с реальными сервисами или базами данных.

import { fetchData } from './api';
jest.mock('./api');

test('mock fetchData in async test', async () => {
  fetchData.mockResolvedValue({ data: 'Test data' });
  
  const response = await fetchData();
  expect(response.data).toBe('Test data');
});

Здесь метод fetchData подменяется на мок-версию, которая возвращает промис с заданным результатом, что позволяет протестировать асинхронное поведение без реальных сетевых запросов.

Работа с модулями, использующими именованные экспорты

При работе с модулями, которые используют именованные экспорты, jest.mock() также позволяет легко подменять эти экспорты. Для этого можно указать точное имя экспортируемой функции или объекта.

// utils.js
export const calculatePrice = (price, tax) => price + tax;

// test.js
import { calculatePrice } from './utils';

jest.mock('./utils', () => ({
  calculatePrice: jest.fn().mockReturnValue(100)
}));

test('mock calculatePrice function', () => {
  expect(calculatePrice(50, 10)).toBe(100);
});

В этом примере jest.mock() заменяет только функцию calculatePrice, оставив другие функции модуля без изменений.

Мокинг модулей с экспортом объектов и функций

Модули могут экспортировать как объекты, так и функции. Важно учитывать, что для подмены таких модулей может понадобиться немного больше усилий, так как нужно заменить не только методы, но и свойства объекта.

// logger.js
export const logger = {
  info: (message) => console.log(message),
  error: (message) => console.error(message)
};

// test.js
import { logger } from './logger';

jest.mock('./logger', () => ({
  logger: {
    info: jest.fn(),
    error: jest.fn()
  }
}));

test('mock logger methods', () => {
  logger.info('Test info');
  expect(logger.info).toHaveBeenCalledWith('Test info');
});

В случае с объектами мокирование предполагает замену всех методов объекта, что может быть полезно, когда необходимо подменить конкретные аспекты работы модуля, но оставить остальные без изменений.

Использование jest.mock() с внешними библиотеками

Иногда бывает необходимо подменить поведение внешних библиотек. Например, если приложение использует стороннюю библиотеку для работы с датами, такую как moment.js, то можно использовать jest.mock() для замены всех вызовов на фиктивные данные.

// date.js
import moment from 'moment';
export const getFormattedDate = () => moment().format('YYYY-MM-DD');

// test.js
import { getFormattedDate } from './date';
import moment from 'moment';

jest.mock('moment', () => jest.fn().mockReturnValue({
  format: jest.fn().mockReturnValue('2023-01-01')
}));

test('mock moment.js', () => {
  expect(getFormattedDate()).toBe('2023-01-01');
});

Здесь moment подменяется на мок, который всегда возвращает заданную строку даты, что позволяет тестировать функции без зависимости от реальной работы библиотеки.

Заключение

Использование jest.mock() в тестировании является мощным инструментом для изоляции тестируемого кода и контроля за зависимостями. Благодаря гибкости этого метода можно создавать как простые, так и сложные моки, которые позволяют сфокусироваться на тестировании бизнес-логики, исключая внешние влияния.