← Назад к вопросам

Как тестировать Redux?

1.7 Middle🔥 251 комментариев
#State Management

Комментарии (1)

🐱
deepseek-v3.2PrepBro AI4 апр. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Подходы к тестированию Redux

Тестирование Redux-приложений — это многоуровневый процесс, который охватывает редьюсеры, экшены, селекторы и middleware. Правильная стратегия тестирования обеспечивает надёжность состояния приложения и предсказуемость его поведения.

1. Тестирование редьюсеров (Reducers)

Редьюсеры — это чистые функции, которые наиболее просто тестировать. Они всегда возвращают новое состояние на основе предыдущего состояния и экшена.

Ключевые аспекты:

  • Проверка возврата нового состояния (не мутация исходного)
  • Обработка начального состояния
  • Обработка неизвестных экшенов
  • Корректность обработки каждого типа экшена
// reducer.js
const initialState = { items: [], loading: false };
function todoReducer(state = initialState, action) {
  switch (action.type) {
    case 'ADD_ITEM':
      return { ...state, items: [...state.items, action.payload] };
    case 'SET_LOADING':
      return { ...state, loading: action.payload };
    default:
      return state;
  }
}

// reducer.test.js
import todoReducer from './reducer';

describe('todoReducer', () => {
  it('should return initial state', () => {
    expect(todoReducer(undefined, {})).toEqual({
      items: [],
      loading: false
    });
  });

  it('should handle ADD_ITEM', () => {
    const state = { items: ['Item 1'], loading: false };
    const action = { type: 'ADD_ITEM', payload: 'Item 2' };
    const result = todoReducer(state, action);
    
    expect(result.items).toEqual(['Item 1', 'Item 2']);
    expect(result.items).not.toBe(state.items); // Проверка иммутабельности
  });

  it('should handle unknown action', () => {
    const state = { items: [], loading: false };
    expect(todoReducer(state, { type: 'UNKNOWN' })).toBe(state);
  });
});

2. Тестирование экшенов (Actions) и Action Creators

Существует два типа экшенов: обычные и асинхронные (с использованием middleware).

Синхронные экшены:

// actions.js
export const addItem = (item) => ({
  type: 'ADD_ITEM',
  payload: item
});

// actions.test.js
import { addItem } from './actions';

describe('actions', () => {
  it('should create ADD_ITEM action', () => {
    const item = 'New todo';
    const expectedAction = {
      type: 'ADD_ITEM',
      payload: item
    };
    expect(addItem(item)).toEqual(expectedAction);
  });
});

Асинхронные экшены (с Redux Thunk):

// asyncActions.js
export const fetchItems = () => async (dispatch) => {
  dispatch({ type: 'SET_LOADING', payload: true });
  try {
    const response = await api.getItems();
    dispatch({ type: 'SET_ITEMS', payload: response.data });
  } catch (error) {
    dispatch({ type: 'SET_ERROR', payload: error.message });
  } finally {
    dispatch({ type: 'SET_LOADING', payload: false });
  }
};

// asyncActions.test.js
import configureMockStore from 'redux-mock-store';
import thunk from 'redux-thunk';

const middlewares = [thunk];
const mockStore = configureMockStore(middlewares);

describe('async actions', () => {
  it('creates SET_ITEMS when fetching is successful', async () => {
    const mockItems = ['item1', 'item2'];
    api.getItems = jest.fn().mockResolvedValue({ data: mockItems });
    
    const expectedActions = [
      { type: 'SET_LOADING', payload: true },
      { type: 'SET_ITEMS', payload: mockItems },
      { type: 'SET_LOADING', payload: false }
    ];
    
    const store = mockStore({});
    await store.dispatch(fetchItems());
    expect(store.getActions()).toEqual(expectedActions);
  });
});

3. Тестирование селекторов (Selectors)

Селекторы часто содержат логику вычислений (мемоизацию через Reselect), поэтому их тестирование критически важно.

// selectors.js
import { createSelector } from 'reselect';

const getItems = state => state.todos.items;
const getFilter = state => state.todos.filter;

export const getFilteredItems = createSelector(
  [getItems, getFilter],
  (items, filter) => items.filter(item => 
    item.toLowerCase().includes(filter.toLowerCase())
  )
);

// selectors.test.js
import { getFilteredItems } from './selectors';

describe('selectors', () => {
  const state = {
    todos: {
      items: ['React', 'Redux', 'JavaScript'],
      filter: 're'
    }
  };

  it('should filter items correctly', () => {
    expect(getFilteredItems(state)).toEqual(['React', 'Redux']);
  });

  it('should memoize results', () => {
    const result1 = getFilteredItems(state);
    const result2 = getFilteredItems(state);
    expect(result1).toBe(result2); // Та же ссылка благодаря мемоизации
  });
});

4. Тестирование компонентов, подключенных к Redux

Для тестирования React-компонентов, использующих Redux, применяются различные подходы:

Интеграционное тестирование с реальным store:

// TodoList.test.js
import { render, screen, fireEvent } from '@testing-library/react';
import { Provider } from 'react-redux';
import configureStore from 'redux-mock-store';
import TodoList from './TodoList';

const mockStore = configureStore([]);

describe('TodoList component', () => {
  it('should render items from store', () => {
    const store = mockStore({
      todos: { items: ['Item 1', 'Item 2'], loading: false }
    });

    render(
      <Provider store={store}>
        <TodoList />
      </Provider>
    );

    expect(screen.getByText('Item 1')).toBeInTheDocument();
    expect(screen.getByText('Item 2')).toBeInTheDocument();
  });

  it('should dispatch ADD_ITEM on button click', () => {
    const store = mockStore({ todos: { items: [], loading: false } });
    
    render(
      <Provider store={store}>
        <TodoList />
      </Provider>
    );

    fireEvent.click(screen.getByText('Add Item'));
    expect(store.getActions()).toEqual([
      { type: 'ADD_ITEM', payload: 'New Item' }
    ]);
  });
});

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

// PresentationalTodoList.test.js
import { render, screen } from '@testing-library/react';
import PresentationalTodoList from './PresentationalTodoList';

describe('PresentationalTodoList', () => {
  it('should render items passed as props', () => {
    const items = ['Item 1', 'Item 2'];
    const onAddItem = jest.fn();
    
    render(
      <PresentationalTodoList 
        items={items} 
        onAddItem={onAddItem} 
      />
    );

    expect(screen.getByText('Item 1')).toBeInTheDocument();
    expect(screen.getAllByRole('listitem')).toHaveLength(2);
  });
});

5. Тестирование middleware

Middleware тестируются через проверку цепочки вызовов и сайд-эффектов.

// loggerMiddleware.js
const loggerMiddleware = store => next => action => {
  console.log('Dispatching:', action.type);
  const result = next(action);
  console.log('Next state:', store.getState());
  return result;
};

// loggerMiddleware.test.js
import loggerMiddleware from './loggerMiddleware';

describe('loggerMiddleware', () => {
  let consoleLogSpy;
  let store;
  let next;
  
  beforeEach(() => {
    consoleLogSpy = jest.spyOn(console, 'log').mockImplementation();
    store = { getState: jest.fn(() => ({ counter: 1 })) };
    next = jest.fn(() => 'result');
  });

  afterEach(() => {
    consoleLogSpy.mockRestore();
  });

  it('should log action and state', () => {
    const action = { type: 'INCREMENT' };
    const middleware = loggerMiddleware(store)(next);
    
    const result = middleware(action);
    
    expect(consoleLogSpy).toHaveBeenCalledWith('Dispatching:', 'INCREMENT');
    expect(next).toHaveBeenCalledWith(action);
    expect(consoleLogSpy).toHaveBeenCalledWith('Next state:', { counter: 1 });
    expect(result).toBe('result');
  });
});

6. Инструменты и библиотеки

Основной стек для тестирования Redux:

  • Jest — фреймворк для тестирования
  • React Testing Library — тестирование компонентов
  • redux-mock-store — создание mock store
  • jest.fn() и jest.spyOn() — создание моков и шпионов
  • Enzyme (альтернатива RTL) — тестирование компонентов
  • redux-saga-test-plan — для тестирования Redux Saga
  • nock или msw — мокирование HTTP-запросов

7. Практические рекомендации

Приоритетность тестов:

  1. Начинайте с редьюсеров — они наиболее важны и просты в тестировании
  2. Тестируйте селекторы, особенно с мемоизацией
  3. Проверяйте асинхронные экшены и middleware
  4. Пишите интеграционные тесты для критических сценариев

Советы по организации:

  • Используйте describe для группировки связанных тестов
  • Подготовка данных через beforeEach для избежания дублирования
  • Тестируйте крайние случаи (пустые данные, ошибки, пограничные значения)
  • Проверяйте иммутабельность во всех редьюсерах
  • Используйте snapshot-тестирование для сложных структур состояния

Производительность тестов:

  • Изолируйте тесты друг от друга
  • Используйте моки для внешних зависимостей
  • Избегайте создания реального store в юнит-тестах
  • Для интеграционных тестов настраивайте store ближе к production-конфигурации

Глубокое тестирование Redux-приложения значительно снижает количество runtime-ошибок, облегчает рефакторинг и обеспечивает долгосрочную стабильность кодовой базы. Комбинируя разные уровни тестирования, вы создаёте надёжную защиту от регрессий и уверенность в корректности управления состоянием приложения.