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

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


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

В Preact используются те же концепции, что и в React, поэтому для тестирования хуков применяются схожие стратегии:

  1. Юнит-тесты отдельных функций-хуков Когда хук реализует чистую логику (например, управление состоянием или вычисление значений), его можно тестировать как обычную функцию. Это позволяет изолировать логику от рендеринга компонентов.

  2. Тестирование через вспомогательный компонент Для тестов, где важно наблюдать взаимодействие хуков с жизненным циклом компонента, создается минимальный компонент-обертка, который использует тестируемый хук. Это позволяет:

    • Симулировать ререндеры.
    • Проверять поведение побочных эффектов (useEffect).
    • Отслеживать состояние и возвратные значения.

Инструменты для тестирования

Для Preact подходят следующие библиотеки:

  • Jest – основной инструмент для юнит-тестирования и моков функций.
  • @testing-library/preact – специализированная библиотека для тестирования компонентов и хуков Preact с удобными утилитами для рендеринга, событий и проверок.
  • preact-hooks-testing-library – облегчает изолированное тестирование хуков без создания компонента-обертки.

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

npm install --save-dev jest @testing-library/preact

Тестирование состояния с useState

useState является базовым хуком для управления состоянием. Тестирование его логики обычно включает:

  1. Инициализацию состояния.
  2. Вызов функции обновления состояния.
  3. Проверку изменения значения.

Пример теста:

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

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

test('useCounter увеличивает счетчик', () => {
  const { result } = renderHook(() => useCounter(5));
  
  expect(result.current.count).toBe(5);
  
  act(() => {
    result.current.increment();
  });
  
  expect(result.current.count).toBe(6);
});

Ключевые моменты:

  • Использование act для оборачивания действий, изменяющих состояние.
  • Проверка начального и конечного значения после обновлений.

Тестирование побочных эффектов с useEffect

useEffect управляет побочными эффектами компонентов. Для тестирования важно учитывать порядок вызовов и очистку эффектов.

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

import { renderHook, act } from '@testing-library/preact-hooks';
import { useEffect, useState } from 'preact/hooks';

function useTimer() {
  const [seconds, setSeconds] = useState(0);

  useEffect(() => {
    const interval = setInterval(() => setSeconds(s => s + 1), 1000);
    return () => clearInterval(interval);
  }, []);

  return seconds;
}

jest.useFakeTimers();

test('useTimer увеличивает секунды с интервалом', () => {
  const { result } = renderHook(() => useTimer());

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

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

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

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

  expect(result.current).toBe(3);
});

Особенности:

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

Тестирование контекста с useContext

Для хуков, взаимодействующих с контекстом, создается провайдер в тесте:

import { createContext } from 'preact';
import { renderHook } from '@testing-library/preact-hooks';
import { useContext } from 'preact/hooks';

const ThemeContext = createContext('light');

function useTheme() {
  return useContext(ThemeContext);
}

test('useTheme получает значение контекста', () => {
  const wrapper = ({ children }) => (
    <ThemeContext.Provider value="dark">{children}</ThemeContext.Provider>
  );

  const { result } = renderHook(() => useTheme(), { wrapper });
  expect(result.current).toBe('dark');
});

Заметки:

  • Создание тестового wrapper позволяет передать нужное значение контекста.
  • Тестируется корректность получения данных через хук без необходимости рендерить полный компонент.

Мокинг зависимостей и внешних эффектов

При работе с хуками, которые используют внешние сервисы или API, важно изолировать тест:

  • Моки асинхронных запросов (fetch, axios) через Jest.
  • Замена глобальных объектов (например, localStorage) тестовыми версиями.
  • Использование jest.fn() для проверки вызовов функций обратного вызова.

Пример мокинга fetch:

global.fetch = jest.fn(() =>
  Promise.resolve({ json: () => Promise.resolve({ data: 42 }) })
);

test('useFetchData возвращает данные', async () => {
  const { result, waitForNextUpdate } = renderHook(() => useFetchData('/api/data'));

  await waitForNextUpdate();

  expect(result.current.data).toEqual({ data: 42 });
  expect(fetch).toHaveBeenCalledWith('/api/data');
});

Ререндер и проверка зависимостей

Для хуков, зависящих от аргументов или пропсов, тестирование ререндеров позволяет убедиться в правильной работе мемоизации и useEffect:

const { result, rerender } = renderHook(({ value }) => useCustomHook(value), {
  initialProps: { value: 10 }
});

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

rerender({ value: 15 });

expect(result.current).toBe(30);
  • rerender позволяет симулировать изменение входных данных без пересоздания хука с нуля.
  • Проверка правильной реакции на обновленные зависимости.

Стратегии покрытия тестами

  • Юнит-тесты для чистой логики хуков (useState, вычисления значений).
  • Интеграционные тесты для проверки взаимодействия хуков внутри компонентов.
  • Тестирование побочных эффектов для useEffect и асинхронных операций.
  • Контекст и провайдеры для проверки корректного потребления данных.

Эти подходы позволяют создавать надежные и поддерживаемые приложения на Preact с предсказуемым поведением хуков.