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По этой причине тесты оптимизаций используются точечно, в местах с подтверждённой проблемой производительности.
useMemoПроверка наличия useMemo в коде
// Неверно
expect(Component.toString()).toContain('useMemo');
Тестирование реализации вместо поведения
// Неверно
jest.spyOn(React, 'useMemo');
Подобные проверки не отражают реального эффекта оптимизации и нарушают инкапсуляцию.
useMemo с useCallbackconst onCl ick = useCallback(() => {
compute(items);
}, [items]);
Тестирование строится аналогично: фиксируется количество вызовов функции при повторных рендерах и событиях, не меняющих зависимости.
React Testing Library не предоставляет прямых инструментов для анализа производительности. Её задача — обеспечить корректный рендеринг и взаимодействие. Проверка оптимизаций достигается через:
rerender);React.memo);Такой подход сохраняет тесты устойчивыми и приближенными к реальному использованию компонентов.