Тестирование useMemo оптимизаций

useMemo фиксирует результат вычислений между рендерами при неизменности зависимостей. Оптимизация снижает количество пересчётов, но не изменяет внешний контракт компонента. Тестирование концентрируется не на самом факте вызова useMemo, а на наблюдаемом поведении: количестве вычислений, стабильности ссылок и отсутствии лишних перерисовок дочерних компонентов.

В контексте React Testing Library проверка строится вокруг пользовательских сценариев и эффектов в DOM, а не внутренних деталей реализации. Это особенно важно для useMemo, так как тесты, завязанные на реализацию, легко ломаются при рефакторинге.


Типовые сценарии оптимизации с useMemo

Дорогостоящие вычисления

const ExpensiveList = ({ items, filter }) => {
  const filteredItems = useMemo(() => {
    return items.filter(item => item.includes(filter));
  }, [items, filter]);

  return (
    <ul>
      {filteredItems.map(item => (
        <li key={item}>{item}</li>
      ))}
    </ul>
  );
};

Стабилизация ссылок для дочерних компонентов

const Parent = ({ value }) => {
  const config = useMemo(() => ({ value }), [value]);
  return <Child config={config} />;
};

В обоих случаях оптимизация направлена на уменьшение лишней работы React, но визуальный результат остаётся тем же.


Проверка отсутствия лишних пересчётов

Прямой доступ к количеству вызовов useMemo отсутствует. Тестирование строится через побочные эффекты, связанные с вычислениями.

Инструментирование вычислений

const compute = jest.fn(items => items.length);

const Component = ({ items }) => {
  const result = useMemo(() => compute(items), [items]);
  return <span>{result}</span>;
};

Тест

import { render, rerender } from '@testing-library/react';

test('мемоизированное вычисление не пересчитывается без изменения зависимостей', () => {
  const items = ['a', 'b'];

  const { rerender } = render(<Component items={items} />);
  rerender(<Component items={items} />);

  expect(compute).toHaveBeenCalledTimes(1);
});

Фокус находится на количестве вызовов вычисляющей функции, а не на useMemo как таковом.


Изменение зависимостей и повторный пересчёт

test('вычисление пересчитывается при изменении зависимостей', () => {
  const { rerender } = render(<Component items={['a']} />);
  rerender(<Component items={['a', 'b']} />);

  expect(compute).toHaveBeenCalledTimes(2);
});

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


Проверка стабильности ссылок

Стабильность ссылок важна при передаче пропсов в мемоизированные дочерние компоненты.

const Child = React.memo(({ config }) => {
  return <div>{config.value}</div>;
});

const renderSpy = jest.fn();

const TrackedChild = React.memo(props => {
  renderSpy();
  return <Child {...props} />;
});
test('useMemo сохраняет ссылку объекта между рендерами', () => {
  const { rerender } = render(<Parent value={1} />);
  rerender(<Parent value={1} />);

  expect(renderSpy).toHaveBeenCalledTimes(1);
});

Здесь проверяется эффект оптимизации: отсутствие повторного рендера дочернего компонента.


Отличие тестирования оптимизаций от функциональных тестов

Функциональный тест проверяет, что компонент корректно отображает данные:

expect(screen.getByText('2')).toBeInTheDocument();

Тест оптимизации проверяет производственное поведение:

  • количество вызовов вычислений;
  • количество ререндеров;
  • стабильность ссылок.

Оба вида тестов существуют параллельно и решают разные задачи.


Ограничения и риски тестирования useMemo

  • В среде тестирования React может рендерить компоненты иначе, чем в продакшене (Strict Mode).
  • Проверка количества ререндеров повышает хрупкость тестов.
  • Оптимизация не гарантирует улучшения производительности, если вычисления тривиальны.

По этой причине тесты оптимизаций используются точечно, в местах с подтверждённой проблемой производительности.


Антипаттерны при тестировании useMemo

Проверка наличия useMemo в коде

// Неверно
expect(Component.toString()).toContain('useMemo');

Тестирование реализации вместо поведения

// Неверно
jest.spyOn(React, 'useMemo');

Подобные проверки не отражают реального эффекта оптимизации и нарушают инкапсуляцию.


Комбинация useMemo с useCallback

const onCl ick = useCallback(() => {
  compute(items);
}, [items]);

Тестирование строится аналогично: фиксируется количество вызовов функции при повторных рендерах и событиях, не меняющих зависимости.


Роль React Testing Library в тестировании оптимизаций

React Testing Library не предоставляет прямых инструментов для анализа производительности. Её задача — обеспечить корректный рендеринг и взаимодействие. Проверка оптимизаций достигается через:

  • повторные рендеры (rerender);
  • мок-функции;
  • мемоизированные компоненты (React.memo);
  • косвенные наблюдаемые эффекты.

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