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, и методами для работы с асинхронными
задачами, можно создать надежные и гибкие тесты для функциональных
частей приложения.