Тестирование useReducer

useReducer — это хук в React, который позволяет управлять состоянием компонента с помощью редьюсера, что делает его особенно полезным для более сложных случаев, когда простого хука useState недостаточно. При написании тестов для компонентов, использующих useReducer, важно убедиться в корректной работе всех частей: редьюсера, диспетчера и самого состояния. В этой статье рассмотрим, как проводить тестирование компонентов, использующих useReducer, с помощью библиотеки Enzyme.

Основы использования useReducer

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

function reducer(state, action) {
  switch (action.type) {
    case 'increment':
      return { count: state.count + 1 };
    case 'decrement':
      return { count: state.count - 1 };
    default:
      return state;
  }
}

В компоненте использование useReducer выглядит так:

const [state, dispatch] = useReducer(reducer, { count: 0 });

Здесь state — это текущее состояние, а dispatch — функция, которая вызывает редьюсер с определенным действием.

Подготовка теста

Для тестирования компонента, использующего useReducer, важно настроить Enzyme и правильно подготовить компонент. Обычно для тестирования компонентов используется shallow рендеринг, поскольку он позволяет тестировать компонент изолированно, без рендеринга всего дерева дочерних компонентов. Важно помнить, что при тестировании с useReducer также важно проверять корректность вызовов функции dispatch.

Пример компонента с useReducer

Рассмотрим простой компонент, который использует useReducer для управления счетчиком:

import React, { useReducer } from 'react';

const reducer = (state, action) => {
  switch (action.type) {
    case 'increment':
      return { count: state.count + 1 };
    case 'decrement':
      return { count: state.count - 1 };
    default:
      return state;
  }
};

const Counter = () => {
  const [state, dispatch] = useReducer(reducer, { count: 0 });

  return (
    <div>
      <p>{state.count}</p>
      <button onCl ick={() => dispatch({ type: 'increment' })}>Increment</button>
      <button onCl ick={() => dispatch({ type: 'decrement' })}>Decrement</button>
    </div>
  );
};

export default Counter;

Тестирование компонента

Для тестирования этого компонента можно использовать Enzyme. Пример теста, который проверяет правильность работы кнопок и редьюсера:

import { shallow } from 'enzyme';
import Counter from './Counter';

describe('Counter', () => {
  it('отображает начальное состояние', () => {
    const wrapper = shallow(<Counter />);
    expect(wrapper.find('p').text()).toBe('0');
  });

  it('увеличивает счетчик при клике на кнопку Increment', () => {
    const wrapper = shallow(<Counter />);
    wrapper.find('button').at(0).simulate('click');
    expect(wrapper.find('p').text()).toBe('1');
  });

  it('уменьшает счетчик при клике на кнопку Decrement', () => {
    const wrapper = shallow(<Counter />);
    wrapper.find('button').at(1).simulate('click');
    expect(wrapper.find('p').text()).toBe('-1');
  });
});

Здесь важно, что при каждом клике на кнопки Increment и Decrement вызывается функция dispatch, которая передает соответствующее действие в редьюсер. После этого компонент перерисовывается, и мы проверяем обновление состояния.

Тестирование редьюсера отдельно

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

Пример теста для редьюсера:

import reducer from './reducer';

describe('reducer', () => {
  it('обрабатывает действие increment', () => {
    const initialState = { count: 0 };
    const action = { type: 'increment' };
    const newState = reducer(initialState, action);
    expect(newState.count).toBe(1);
  });

  it('обрабатывает действие decrement', () => {
    const initialState = { count: 1 };
    const action = { type: 'decrement' };
    const newState = reducer(initialState, action);
    expect(newState.count).toBe(0);
  });

  it('возвращает начальное состояние для неизвестных действий', () => {
    const initialState = { count: 0 };
    const action = { type: 'unknown' };
    const newState = reducer(initialState, action);
    expect(newState.count).toBe(0);
  });
});

Этот тест проверяет работу редьюсера без зависимости от рендеринга компонента и без необходимости взаимодействовать с dispatch.

Тестирование состояния через useReducer с асинхронными действиями

Если ваш компонент использует асинхронные операции, например, API-запросы или таймеры, тестировать такие компоненты нужно с учетом асинхронности. Для этого можно использовать jest.mock для подмены асинхронных функций или мокирования API.

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

import React, { useReducer, useEffect } from 'react';

const reducer = (state, action) => {
  switch (action.type) {
    case 'fetchSuccess':
      return { ...state, data: action.payload };
    default:
      return state;
  }
};

const AsyncComponent = () => {
  const [state, dispatch] = useReducer(reducer, { data: null });

  useEffect(() => {
    setTimeout(() => {
      dispatch({ type: 'fetchSuccess', payload: 'data from API' });
    }, 1000);
  }, []);

  return (
    <div>
      <p>{state.data || 'Loading...'}</p>
    </div>
  );
};

export default AsyncComponent;

Тестирование этого компонента:

import { shallow } from 'enzyme';
import AsyncComponent from './AsyncComponent';

jest.useFakeTimers();

describe('AsyncComponent', () => {
  it('отображает данные после асинхронного запроса', () => {
    const wrapper = shallow(<AsyncComponent />);
    expect(wrapper.find('p').text()).toBe('Loading...');
    
    jest.runAllTimers(); // Запускаем все таймеры

    wrapper.update(); // Обновляем компонент
    expect(wrapper.find('p').text()).toBe('data from API');
  });
});

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

Заключение

Тестирование компонентов, использующих useReducer, важно для обеспечения надежности и правильности работы состояния и редьюсеров. С помощью Enzyme можно легко проверять как взаимодействие с функцией dispatch, так и логику редьюсера. Важно не забывать о тестировании состояния при асинхронных операциях и отделять логику редьюсера для изоляции тестов.