Тестирование хуков

Хуки — важная часть функциональности React-приложений, позволяющая работать с состоянием, эффектами и контекстом без необходимости использования классов. Тестирование хуков требует особого подхода, поскольку они часто изменяют поведение компонентов и взаимодействуют с их состоянием. Jest, в сочетании с библиотеками для тестирования, такими как React Testing Library, предоставляет мощные инструменты для тестирования этих функциональных элементов.

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

Тестирование хуков отличается от тестирования обычных компонентов. Хуки сами по себе не рендерят интерфейс, а управляют состоянием или побочными эффектами. Таким образом, для тестирования хуков важно проверять их внутренние состояния, взаимодействие с другими хуками и их реакцию на изменения.

Тесты хуков должны быть изолированы от компонентов, чтобы проверять только логику хуков, не взаимодействуя с визуальными аспектами. Для этого используются вспомогательные библиотеки, такие как @testing-library/react-hooks.

Тестирование синхронных хуков

Синхронные хуки, такие как useState, useReducer, useCallback, useMemo, не требуют дополнительных ожиданий или асинхронных операций для работы. Они изменяют состояние или возвращают функции, которые могут быть протестированы простыми ассерциями.

Пример тестирования хука useState:

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

function useCounter() {
  const [count, setCount] = useState(0);
  const increment = () => setCount(count + 1);
  return { count, increment };
}

test('должен увеличивать счетчик', () => {
  const { result } = renderHook(() => useCounter());

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

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

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

В этом примере:

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

Тестирование асинхронных хуков

Асинхронные хуки, такие как useEffect или пользовательские хуки, выполняющие асинхронные запросы, требуют использования промисов или колбеков для проверки их работы. Для тестирования таких хуков часто нужно ожидать их завершения, прежде чем делать проверки.

Пример тестирования хука с асинхронным запросом:

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

function useFetchData(url) {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    const fetchData = async () => {
      const response = await fetch(url);
      const result = await response.json();
      setData(result);
      setLoading(false);
    };

    fetchData();
  }, [url]);

  return { data, loading };
}

test('должен загружать данные', async () => {
  global.fetch = jest.fn(() =>
    Promise.resolve({ json: () => Promise.resolve({ message: 'Hello' }) })
  );

  const { result, waitForNextUpdate } = renderHook(() => useFetchData('/api/data'));

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

  await waitForNextUpdate();

  expect(result.current.loading).toBe(false);
  expect(result.current.data).toEqual({ message: 'Hello' });
});

Здесь:

  • Мокается глобальный fetch, чтобы симулировать асинхронный запрос.
  • waitForNextUpdate используется для ожидания завершения асинхронного обновления состояния.

Мокинг зависимостей

Многие хуки зависят от внешних API или библиотек. В таких случаях необходимо мокать эти зависимости для изоляции тестов и обеспечения их предсказуемости.

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

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

function useMyContext() {
  const context = useContext(MyContext);
  return context;
}

test('должен использовать контекстное значение', () => {
  const wrapper = ({ children }) => (
    <MyContext.Provider value="Test Value">{children}</MyContext.Provider>
  );

  const { result } = renderHook(() => useMyContext(), { wrapper });

  expect(result.current).toBe('Test Value');
});

Здесь:

  • Контекст MyContext мокается через проп wrapper в renderHook, что позволяет подставить необходимые данные.
  • Важное замечание: контекст должен быть предоставлен для хука, иначе он вернет undefined.

Тестирование пользовательских хуков

Пользовательские хуки, которые используют несколько других хуков, требуют тестирования как их внутреннего состояния, так и корректности логики. В таких случаях можно комбинировать подходы, описанные выше.

Пример тестирования пользовательского хука, который использует useState и useEffect:

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

function useDelayedState(initialState, delay) {
  const [state, setState] = useState(initialState);

  useEffect(() => {
    const timer = setTimeout(() => setState('Updated'), delay);
    return () => clearTimeout(timer);
  }, [delay]);

  return state;
}

test('должен обновить состояние через заданную задержку', () => {
  jest.useFakeTimers();

  const { result } = renderHook(() => useDelayedState('Initial', 1000));

  expect(result.current).toBe('Initial');

  act(() => {
    jest.advanceTimersByTime(1000);
  });

  expect(result.current).toBe('Updated');
});

Здесь:

  • jest.useFakeTimers() используется для манипуляции временем в тестах.
  • act необходим для синхронизации и обеспечения корректных обновлений состояния.

Работа с хук-эффектами

Хук useEffect — один из наиболее часто тестируемых хуков. Он позволяет выполнять побочные эффекты в компонентах, например, загрузку данных или подписку на события. Для его тестирования важно убедиться, что эффекты выполняются правильно и в нужный момент.

Пример тестирования useEffect:

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

function useEffectExample() {
  const [value, setValue] = useState(null);

  useEffect(() => {
    const timer = setTimeout(() => setValue('Hello'), 1000);
    return () => clearTimeout(timer);
  }, []);

  return value;
}

test('должен обновить состояние через 1 секунду', () => {
  jest.useFakeTimers();

  const { result } = renderHook(() => useEffectExample());

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

  act(() => {
    jest.advanceTimersByTime(1000);
  });

  expect(result.current).toBe('Hello');
});

Здесь:

  • useFakeTimers позволяет тестировать хук с использованием таймеров, что полезно для асинхронных эффектов.
  • act нужен для безопасного выполнения всех побочных эффектов перед проверкой результата.

Заключение

Тестирование хуков в Jest требует особого внимания к синхронности и асинхронности, а также необходимости мока внешних зависимостей. Важно помнить, что хуки не рендерят UI, а только управляют состоянием и побочными эффектами, поэтому тесты должны быть сфокусированы на проверке их логики. Использование таких инструментов, как renderHook, act, и мокирование зависимостей позволяет создавать точные и надежные тесты для хуков.