RTL renderHook и другие вспомогательные

React Testing Library (RTL) предоставляет мощный набор инструментов для тестирования компонентов, разработанных с использованием React. Однако, помимо стандартных функций для рендеринга компонентов, существует ряд вспомогательных утилит, которые делают процесс тестирования более гибким и удобным. Одной из таких утилит является функция renderHook, которая предназначена для тестирования кастомных хуков в React.

Основы использования renderHook

В отличие от обычной функции рендеринга, которая применяется к компонентам React, renderHook используется для тестирования функциональных хуков. Она позволяет проверить логику внутри хуков без необходимости создавать полноценные компоненты для их тестирования.

Для использования renderHook необходимо импортировать его из библиотеки @testing-library/react-hooks. Эта функция возвращает объект с несколькими полезными методами, которые можно использовать для тестирования результата работы хука.

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

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

function useCounter() {
  const [count, setCount] = React.useState(0);
  
  const increment = () => setCount(count + 1);
  const decrement = () => setCount(count - 1);

  return { count, increment, decrement };
}

test('проверка хука useCounter', () => {
  const { result } = renderHook(() => useCounter());

  expect(result.current.count).toBe(0);
  
  act(() => {
    result.current.increment();
  });
  
  expect(result.current.count).toBe(1);

  act(() => {
    result.current.decrement();
  });
  
  expect(result.current.count).toBe(0);
});

В данном примере мы тестируем хук useCounter, который управляет состоянием счетчика. Сначала мы проверяем начальное значение счетчика, затем вызываем методы increment и decrement, тестируя их поведение.

Работа с состоянием и эффектами

Для более сложных хуков, которые используют useEffect или другие побочные эффекты, React Testing Library также предоставляет поддержку через act(). Функция act() помогает синхронизировать тесты с рендером и обновлениями компонента или хука.

Пример использования хука с эффектом:

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

function useTimer() {
  const [time, setTime] = React.useState(0);

  React.useEffect(() => {
    const interval = setInterval(() => setTime((t) => t + 1), 1000);
    return () => clearInterval(interval);
  }, []);

  return time;
}

test('проверка хука useTimer', () => {
  const { result } = renderHook(() => useTimer());

  expect(result.current).toBe(0);

  // ждем, пока время изменится
  act(() => {
    jest.advanceTimersByTime(1000);
  });

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

Здесь мы тестируем хук, который обновляет состояние каждую секунду с помощью setInterval. Функция jest.advanceTimersByTime() позволяет продлить время в тесте и проверить, как хук реагирует на изменения.

Взаимодействие с асинхронными операциями

Для тестирования хуков, которые выполняют асинхронные операции, таких как запросы к API или использование useEffect с асинхронными функциями, можно воспользоваться waitFor из RTL.

Пример асинхронного хука:

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

function useFetchData() {
  const [data, setData] = React.useState(null);
  
  React.useEffect(() => {
    const fetchData = async () => {
      const response = await fetch('/api/data');
      const result = await response.json();
      setData(result);
    };
    
    fetchData();
  }, []);
  
  return data;
}

test('проверка асинхронного хука useFetchData', async () => {
  global.fetch = jest.fn(() =>
    Promise.resolve({
      json: () => Promise.resolve({ name: 'Test' }),
    })
  );

  const { result, waitForNextUpdate } = renderHook(() => useFetchData());

  await waitForNextUpdate();

  expect(result.current).toEqual({ name: 'Test' });
});

В этом примере тестируется хук useFetchData, который выполняет асинхронный запрос и обновляет состояние после получения данных. Важно использовать waitForNextUpdate(), чтобы дождаться обновления состояния в асинхронной функции.

Использование rerender и обновление хуков

При тестировании хуков часто бывает необходимо обновлять параметры, передаваемые в хук, и проверять, как они влияют на результат работы. Для таких случаев renderHook предоставляет метод rerender, который позволяет повторно вызвать хук с новыми параметрами.

Пример с rerender:

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

function useToggle(initialValue = false) {
  const [value, setValue] = React.useState(initialValue);
  const toggle = () => setValue(!value);
  
  return { value, toggle };
}

test('проверка хука useToggle с обновлением параметров', () => {
  const { result, rerender } = renderHook(
    ({ value }) => useToggle(value),
    { initialProps: { value: false } }
  );

  expect(result.current.value).toBe(false);

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

  expect(result.current.value).toBe(true);

  rerender({ value: false });

  expect(result.current.value).toBe(false);
});

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

Утилиты для работы с хранилищами состояний

Если хук работает с контекстами или состоянием приложения (например, через Redux или useContext), можно комбинировать renderHook с обертками, которые обеспечат правильную инициализацию состояния.

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

import { renderHook } from '@testing-library/react-hooks';
import { MyContextProvider, useMyContext } from './MyContext';

test('проверка хука с контекстом', () => {
  const { result } = renderHook(() => useMyContext(), {
    wrapper: MyContextProvider,
  });

  expect(result.current).toBe('some context value');
});

В этом примере хук useMyContext использует контекст, обернутый в MyContextProvider. Это позволяет изолированно тестировать поведение хука в рамках определенного контекста.

Итоги

Функция renderHook предоставляет множество возможностей для тестирования React-хуков. Ее использование позволяет эффективно изолировать и проверять логику внутри хуков, независимо от компонентов. В сочетании с такими инструментами, как act, waitForNextUpdate, и методами для работы с асинхронными задачами, можно создать надежные и гибкие тесты для функциональных частей приложения.