renderHook функция и её использование

В тестировании React-приложений особую важность приобретает проверка логики хуков. Стандартные подходы с рендерингом компонентов не всегда удобны, особенно когда требуется тестировать пользовательские хуки отдельно от UI. Для этого используется функция renderHook из библиотеки @testing-library/react-hooks, которая позволяет изолированно вызывать и проверять хуки.


Основы renderHook

renderHook принимает на вход функцию, возвращающую хук. Эта функция выполняется так, как если бы хук был вызван внутри компонента. Возвращаемое значение функции renderHook — объект с ключевыми свойствами для взаимодействия с хуком:

  • result — объект, содержащий текущее состояние хука в result.current.
  • rerender — функция для повторного рендеринга с новыми аргументами.
  • unmount — функция для размонтирования хука.
  • waitForNextUpdate — промис, который ожидает следующего обновления состояния.

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

import { renderHook, act } from '@testing-library/react-hooks';
import { useState } from 'react';

function useCounter(initialValue = 0) {
  const [count, setCount] = useState(initialValue);
  const increment = () => setCount(c => c + 1);
  return { count, increment };
}

const { result } = renderHook(() => useCounter(5));

console.log(result.current.count); // 5
act(() => {
  result.current.increment();
});
console.log(result.current.count); // 6

Ключевой момент: любые действия, изменяющие состояние хука, должны быть обёрнуты в act, чтобы корректно тестировать асинхронные обновления и избежать предупреждений React.


Работа с параметрами хука

Многие хуки принимают аргументы. renderHook позволяет передавать их через функцию:

const { result, rerender } = renderHook(
  ({ initial }) => useCounter(initial),
  { initialProps: { initial: 10 } }
);

console.log(result.current.count); // 10

rerender({ initial: 20 });
console.log(result.current.count); // 20
  • initialProps задаёт начальные аргументы.
  • rerender(newProps) обновляет хук с новыми параметрами, что важно для тестирования динамического поведения.

Асинхронные хуки

Для хуков, которые используют useEffect с асинхронными операциями, применяются методы waitFor и waitForNextUpdate. Они позволяют дождаться завершения обновления состояния перед проверкой:

import { useEffect, useState } from 'react';

function useAsyncValue(fetcher) {
  const [value, setValue] = useState(null);

  useEffect(() => {
    let mounted = true;
    fetcher().then(result => {
      if (mounted) setValue(result);
    });
    return () => {
      mounted = false;
    };
  }, [fetcher]);

  return value;
}

const fetcher = () => Promise.resolve(42);
const { result, waitForNextUpdate } = renderHook(() => useAsyncValue(fetcher));

await waitForNextUpdate();
console.log(result.current); // 42

Методы ожидания позволяют корректно тестировать хуки с асинхронными побочными эффектами, избегая гонок и состояния «undefined» при проверке результата.


Тестирование кастомных хуков с контекстом

Некоторые хуки зависят от контекста React. renderHook предоставляет опцию wrapper, которая позволяет обернуть хук в необходимые провайдеры:

import { createContext, useContext } from 'react';

const ThemeContext = createContext('light');

function useTheme() {
  return useContext(ThemeContext);
}

const wrapper = ({ children }) => (
  <ThemeContext.Provider value="dark">{children}</ThemeContext.Provider>
);

const { result } = renderHook(() => useTheme(), { wrapper });
console.log(result.current); // "dark"
  • wrapper — компонент, оборачивающий хук, что позволяет подставлять контексты, роутеры, Redux-провайдеры и другие обёртки.

Проверка изменений состояния

renderHook совместно с act обеспечивает безопасное тестирование любых изменений состояния:

act(() => {
  result.current.increment();
});

expect(result.current.count).toBe(1);

Важно: каждое обновление состояния, связанное с побочными эффектами или событиями, должно быть внутри act. Это синхронизирует состояние с механизмом рендера React.


Использование renderHook для сложных хуков

  • Композиция хуков: один хук вызывает несколько других. renderHook позволяет протестировать конечный результат без необходимости рендерить компонент.
  • Проверка подписок и отписок: через unmount можно проверить, что все побочные эффекты корректно очистились.
  • Тестирование кеширования и мемоизации: через rerender легко проверять, что значения изменяются только при нужных условиях.

Рекомендации по структуре тестов

  1. Создавать отдельные тестовые блоки для каждого состояния хука.
  2. Использовать act для всех изменений состояния.
  3. Для асинхронных операций применять waitForNextUpdate или waitFor.
  4. Использовать wrapper для хуков, зависящих от контекста.
  5. Проверять корректность размонтирования через unmount при тестировании побочных эффектов.

renderHook является мощным инструментом для unit-тестирования хуков в изоляции, позволяя быстро выявлять ошибки логики и асинхронных операций без необходимости рендеринга сложных компонентов.