Кастомные хуки в React используются для инкапсуляции логики состояния, побочных эффектов и повторно используемого поведения. Они не рендерят UI напрямую, а значит, не могут тестироваться так же, как компоненты. Основная задача тестирования кастомных хуков — проверить корректность логики, изменение состояния, обработку эффектов и взаимодействие с внешними зависимостями без привязки к DOM-структуре.
React Testing Library ориентирована на тестирование поведения через пользовательские сценарии, однако для хуков применяется более низкоуровневый подход, при котором хук исполняется в изолированном окружении.
Для тестирования хуков используется пакет:
@testing-library/react
Начиная с версии 13, отдельный пакет
@testing-library/react-hooks считается устаревшим, а его
функциональность перенесена в основной пакет.
Ключевая функция:
renderHook
Она позволяет выполнить кастомный хук в тестовой среде и получить доступ к его текущему состоянию и методам.
import { useState } from 'react';
export function useCounter(initialValue = 0) {
const [count, setCount] = useState(initialValue);
const increment = () => setCount(c => c + 1);
const decrement = () => setCount(c => c - 1);
return { count, increment, decrement };
}
import { renderHook } from '@testing-library/react';
import { useCounter } from './useCounter';
test('возвращает начальное значение', () => {
const { result } = renderHook(() => useCounter(10));
expect(result.current.count).toBe(10);
});
Ключевые моменты:
result.current всегда содержит актуальное состояние
хукаrenderHook изолированВсе операции, изменяющие состояние, должны быть обёрнуты в
act.
import { act } from '@testing-library/react';
test('инкремент увеличивает значение', () => {
const { result } = renderHook(() => useCounter());
act(() => {
result.current.increment();
});
expect(result.current.count).toBe(1);
});
act гарантирует, что все обновления состояния и эффекты
будут обработаны до выполнения assertions.
function useMultiplier(value) {
return value * 2;
}
test('реагирует на изменение входных параметров', () => {
const { result, rerender } = renderHook(
({ value }) => useMultiplier(value),
{ initialProps: { value: 2 } }
);
expect(result.current).toBe(4);
rerender({ value: 5 });
expect(result.current).toBe(10);
});
rerender позволяет имитировать изменение props,
аналогично повторному рендеру компонента.
useEffectimport { useEffect, useState } from 'react';
function useDocumentTitle(title) {
useEffect(() => {
document.title = title;
}, [title]);
}
test('обновляет document.title', () => {
renderHook(() => useDocumentTitle('Test title'));
expect(document.title).toBe('Test title');
});
Важно учитывать побочные эффекты и очищать глобальное состояние после тестов при необходимости.
function useAsyncData() {
const [data, setData] = useState(null);
useEffect(() => {
Promise.resolve('result').then(setData);
}, []);
return data;
}
import { waitFor } from '@testing-library/react';
test('возвращает данные асинхронно', async () => {
const { result } = renderHook(() => useAsyncData());
await waitFor(() => {
expect(result.current).toBe('result');
});
});
waitFor повторяет проверку до выполнения условия или
истечения таймаута.
При использовании API, таймеров или сторонних библиотек применяется мокирование.
jest.spyOn(global, 'fetch').mockResolvedValue({
json: async () => ({ value: 42 }),
});
function useFetchValue() {
const [value, setValue] = useState(null);
useEffect(() => {
fetch('/api').then(r => r.json()).then(data => setValue(data.value));
}, []);
return value;
}
test('загружает значение из API', async () => {
const { result } = renderHook(() => useFetchValue());
await waitFor(() => {
expect(result.current).toBe(42);
});
});
Если хук использует useContext, необходимо предоставить
провайдер.
const ThemeContext = React.createContext('light');
function useTheme() {
return React.useContext(ThemeContext);
}
test('получает значение из контекста', () => {
const wrapper = ({ children }) => (
<ThemeContext.Provider value="dark">
{children}
</ThemeContext.Provider>
);
const { result } = renderHook(() => useTheme(), { wrapper });
expect(result.current).toBe('dark');
});
Для локальной отладки применяются:
console.log внутри хукаdebug() при тестировании
компонентов-обёртокresult.all для анализа истории состоянийexpect(result.all).toEqual([
{ count: 0 },
{ count: 1 },
]);
result.all полезен для анализа последовательных
обновлений состояния.
act при изменении состоянияjest.useFakeTimersКастомные хуки, покрытые корректными тестами, значительно упрощают сопровождение бизнес-логики, повышают предсказуемость поведения приложения и уменьшают связность компонентов.