Partial mocks в React

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

Основная идея

В отличие от полного мокирования, при котором весь модуль подменяется фиктивной реализацией, partial mock позволяет:

  • Мокировать отдельные функции модуля.
  • Сохранять поведение остальных функций.
  • Контролировать конкретные части зависимости без потери интеграции с остальным кодом.

Это особенно актуально при тестировании компонентов, которые используют сторонние утилиты или API-клиенты, где требуется изолировать лишь определённые вызовы.

Создание partial mock с Jest

Jest предоставляет встроенные возможности для частичного мокирования модулей через комбинацию jest.mock() и jest.requireActual().

Пример:

// utils.js
export const fetchData = async () => {
  const response = await fetch('/data');
  return response.json();
};

export const helper = (value) => value * 2;

Если нужно мокировать только fetchData, сохранив helper:

import * as utils from './utils';

jest.mock('./utils', () => ({
  ...jest.requireActual('./utils'), // сохраняем реальные функции
  fetchData: jest.fn(() => Promise.resolve({ data: 123 })), // мокируем только fetchData
}));

test('partial mock fetchData', async () => {
  const result = await utils.fetchData();
  expect(result).toEqual({ data: 123 });
  expect(utils.helper(2)).toBe(4); // реальная функция работает
});

Ключевой момент — использование jest.requireActual(). Это гарантирует, что все функции, не указанные в моках, сохранят оригинальное поведение.

Partial mocks для React-компонентов

В React Testing Library часто возникает необходимость замокировать части компонентов, например, внутренние хелперы или API-запросы, не заменяя весь компонент целиком.

// MyComponent.js
import React, { useEffect, useState } from 'react';
import { fetchData } from './api';

export function MyComponent() {
  const [data, setData] = useState(null);

  useEffect(() => {
    fetchData().then(setData);
  }, []);

  return <div>{data ? data.value : 'Loading...'}</div>;
}

Partial mock для тестирования:

import { render, screen, waitFor } from '@testing-library/react';
import * as api from './api';
import { MyComponent } from './MyComponent';

jest.mock('./api', () => ({
  ...jest.requireActual('./api'),
  fetchData: jest.fn(() => Promise.resolve({ value: 'Mocked!' })),
}));

test('renders mocked data', async () => {
  render(<MyComponent />);
  await waitFor(() => expect(screen.getByText('Mocked!')).toBeInTheDocument());
});

В этом примере fetchData полностью контролируется тестом, а остальные функции в api остаются оригинальными.

Мокирование только отдельных методов объекта

Иногда требуется замокировать не модуль, а методы объекта, передаваемого компоненту через пропсы или контекст. Используется комбинация jest.spyOn() и mockImplementation.

const apiClient = {
  getUser: () => fetch('/user').then(res => res.json()),
  getPosts: () => fetch('/posts').then(res => res.json()),
};

test('partial spy on getUser', async () => {
  const spy = jest.spyOn(apiClient, 'getUser').mockResolvedValue({ id: 1, name: 'Alice' });

  const user = await apiClient.getUser();
  const posts = await apiClient.getPosts();

  expect(user.name).toBe('Alice'); // мок
  expect(posts).toBeDefined(); // реальная функция работает

  spy.mockRestore(); // восстановление оригинального метода
});

Использование jest.spyOn() позволяет:

  • Локально заменять функцию на мок.
  • Сохранять остальные методы объекта без изменений.
  • Восстанавливать оригинальные функции после теста.

Особенности в асинхронных тестах

Partial mocks часто применяются в сочетании с асинхронными вызовами, например, при мокировании API-запросов. Рекомендуется:

  • Использовать mockResolvedValue или mockRejectedValue вместо mockImplementation, чтобы сразу возвращать промис.
  • Оборачивать проверки в waitFor из React Testing Library, чтобы дождаться обновления компонента.

Пример:

jest.spyOn(api, 'fetchData').mockResolvedValue({ value: 'Async Mock' });

render(<MyComponent />);
await waitFor(() => expect(screen.getByText('Async Mock')).toBeInTheDocument());

Советы по использованию partial mocks

  1. Не злоупотреблять — мокировать следует только то, что реально влияет на тест. Чрезмерное мокирование снижает ценность теста.
  2. Восстановление функций — после теста важно использовать mockRestore() для spyOn или сбрасывать моки через jest.resetAllMocks().
  3. Комбинировать с real implementations — сохранение оригинальных функций позволяет писать интеграционные тесты ближе к реальной логике.
  4. Контекст и пропсы — для компонентов, которые зависят от контекста, partial mocks могут применяться к провайдерам и утилитам, не заменяя весь контекст.

Заключение по технике

Partial mocks — мощный инструмент для тонкой настройки тестов. Они позволяют контролировать критические зависимости компонентов, минимально вмешиваясь в остальную функциональность, обеспечивая при этом стабильные и читаемые тесты. Это идеальный компромисс между полным мокированием и интеграционным тестированием.