Возврат значений от hook-функций

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


Основные подходы к тестированию хуков

  1. Через обертку компонента

Наиболее прямой способ — создать тестовый компонент, который использует нужный хук и отображает его значения:

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, избегая прямого обращения к внутреннему состоянию.


  1. Использование 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(), чтобы корректно отрабатывали обновления и ререндеры.
  • Такой подход упрощает проверку возвращаемых значений и функций, не рендеря лишние DOM-элементы.

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

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

Рекомендации по тестированию возвращаемых значений

  1. Всегда проверять начальное состояние хуков. Это помогает быстро выявить ошибки инициализации.
  2. Функции обновления состояния должны тестироваться отдельно от рендеринга.
  3. Асинхронные хуки тестировать через waitFor или findBy* методы, чтобы учитывать время обновления состояния.
  4. Если хук зависит от внешних контекстов или провайдеров (Context, Redux, Router), использовать специальные обертки (wrapper) в renderHook.

Возврат значений от хуков — это основа для построения надёжных тестов функциональных компонентов. Правильное использование renderHook, act и инструментов RTL позволяет полностью покрыть как синхронные, так и асинхронные сценарии, не вдаваясь в детали реализации внутренних состояний React.