При тестировании React-компонентов с использованием React Testing Library (RTL) важной задачей является проверка работы хуков (hooks), особенно если они возвращают значения, влияющие на рендеринг компонентов. В отличие от классовых компонентов, где состояние и методы можно тестировать через экземпляр компонента, хуки требуют другого подхода, так как они не существуют вне функционального компонента.
Наиболее прямой способ — создать тестовый компонент, который использует нужный хук и отображает его значения:
import { render, screen } from '@testing-library/react';
import { useState } from 'react';
function useCounter(initialValue = 0) {
const [count, setCount] = useState(initialValue);
const increment = () => setCount(c => c + 1);
return { count, increment };
}
function TestComponent({ initialValue }) {
const { count, increment } = useCounter(initialValue);
return (
<div>
<span data-testid="count">{count}</span>
<button onCl ick={increment}>Increment</button>
</div>
);
}
test('useCounter возвращает корректное начальное значение', () => {
render(<TestComponent initialValue={5} />);
const countElement = screen.getByTestId('count');
expect(countElement.textContent).toBe('5');
});
Ключевой момент: тест проверяет именно значение, возвращаемое хуком, через рендеринг компонента, что позволяет оставаться в рамках React Testing Library, избегая прямого обращения к внутреннему состоянию.
renderHook из
@testing-library/react-hooksДля более прямого тестирования хуков без создания вспомогательных
компонентов используется библиотека
@testing-library/react-hooks:
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 };
}
test('useCounter увеличивает значение count', () => {
const { result } = renderHook(() => useCounter(2));
expect(result.current.count).toBe(2);
act(() => {
result.current.increment();
});
expect(result.current.count).toBe(3);
});
Важные моменты при использовании
renderHook:
result.current всегда содержит актуальные значения,
возвращаемые хуком.act(),
чтобы корректно отрабатывали обновления и ререндеры.Если хук возвращает значения после асинхронной операции, необходимо использовать утилиты RTL для ожидания:
import { renderHook, waitFor } from '@testing-library/react';
import { useEffect, useState } from 'react';
function useFetchData(url) {
const [data, setData] = useState(null);
useEffect(() => {
fetch(url)
.then(res => res.json())
.then(setData);
}, [url]);
return data;
}
test('useFetchData возвращает данные после fetch', async () => {
global.fetch = jest.fn(() =>
Promise.resolve({ json: () => Promise.resolve({ name: 'Test' }) })
);
const { result } = renderHook(() => useFetchData('/api/data'));
await waitFor(() => expect(result.current).toEqual({ name: 'Test' }));
});
Особенности:
waitFor позволяет дождаться обновления состояния после
асинхронного запроса.fetch или других API-методов предотвращает
сетевые вызовы во время тестов.Хуки часто возвращают не только примитивные значения, но и объекты или функции:
function useForm(initialValues) {
const [values, setValues] = useState(initialValues);
const updateField = (field, value) =>
setValues(prev => ({ ...prev, [field]: value }));
return { values, updateField };
}
test('useForm обновляет поле', () => {
const { result } = renderHook(() => useForm({ name: '' }));
act(() => {
result.current.updateField('name', 'Alice');
});
expect(result.current.values).toEqual({ name: 'Alice' });
});
Ключевые принципы:
act().toEqual или toMatchObject.waitFor или
findBy* методы, чтобы учитывать время обновления
состояния.Context, Redux, Router),
использовать специальные обертки (wrapper) в
renderHook.Возврат значений от хуков — это основа для построения надёжных тестов
функциональных компонентов. Правильное использование
renderHook,
act и инструментов RTL позволяет полностью
покрыть как синхронные, так и асинхронные сценарии, не вдаваясь в детали
реализации внутренних состояний React.