Тестирование actions и reducers

Enzyme — популярная библиотека для тестирования React-приложений, которая позволяет тестировать компоненты, монтировать их в виртуальный DOM и взаимодействовать с ними. Однако Enzyme также может быть использован для тестирования других частей приложения, таких как actions и reducers в контексте Redux. Важно понимать, что тестирование этих сущностей играет ключевую роль в обеспечении надежности приложения. В данной статье рассмотрены подходы и примеры тестирования actions и reducers с использованием Enzyme.

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

Actions в Redux — это объекты, которые отправляют данные в store. Каждый action должен быть протестирован для того, чтобы убедиться, что он правильно выполняет свою задачу и генерирует корректный результат. Например, проверка того, что action создает правильный объект с правильными типами и данными, является важной частью процесса тестирования.

Для тестирования actions можно использовать различные подходы, но ключевыми являются следующие шаги:

  1. Создание тестов для action creators Action creators — это функции, которые возвращают объекты действия. Тестирование этих функций заключается в проверке того, что они возвращают правильные объекты.

    Пример action creator:

    const setUser = (user) => ({
      type: 'SET_USER',
      payload: user
    });

    Для тестирования этой функции можно использовать Enzyme и Jest:

    describe('setUser action creator', () => {
      it('should create an action to set the user', () => {
        const user = { name: 'John', age: 30 };
        const expectedAction = {
          type: 'SET_USER',
          payload: user
        };
        expect(setUser(user)).toEqual(expectedAction);
      });
    });

    В этом примере создается тест, который проверяет, что action creator setUser возвращает объект с типом SET_USER и переданными данными пользователя.

  2. Тестирование асинхронных actions (Thunk) Redux Thunk позволяет создавать асинхронные action creators, которые могут выполнять побочные эффекты, например, делать запросы к API. Для тестирования таких actions можно использовать mock-функции и проверку их вызовов.

    Пример асинхронного action creator:

    const fetchUser = (userId) => {
      return async (dispatch) => {
        const response = await fetch(`/api/user/${userId}`);
        const user = await response.json();
        dispatch(setUser(user));
      };
    };

    Для тестирования этого асинхронного action creator можно использовать Jest:

    jest.mock('node-fetch');
    const fetch = require('node-fetch');
    const { Response } = jest.requireActual('node-fetch');
    
    describe('fetchUser action creator', () => {
      it('should fetch user data and dispatch setUser', async () => {
        const mockDispatch = jest.fn();
        const mockUser = { name: 'John', age: 30 };
        fetch.mockResolvedValueOnce(new Response(JSON.stringify(mockUser)));
    
        await fetchUser(1)(mockDispatch);
    
        expect(mockDispatch).toHaveBeenCalledWith({
          type: 'SET_USER',
          payload: mockUser
        });
      });
    });

    В этом примере проверяется, что асинхронный action создает правильный action после успешного выполнения API-запроса.

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

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

  1. Тестирование простых reducers Простые reducers можно протестировать с помощью стандартных тестов, проверяя их поведение при различных входных данных.

    Пример reducer:

    const userReducer = (state = {}, action) => {
      switch (action.type) {
        case 'SET_USER':
          return {
            ...state,
            user: action.payload
          };
        default:
          return state;
      }
    };

    Тестирование этого reducer можно выполнить следующим образом:

    describe('userReducer', () => {
      it('should handle SET_USER action', () => {
        const initialState = {};
        const user = { name: 'John', age: 30 };
        const action = {
          type: 'SET_USER',
          payload: user
        };
        const expectedState = { user };
    
        expect(userReducer(initialState, action)).toEqual(expectedState);
      });
    
      it('should return the initial state when action is unknown', () => {
        const initialState = {};
        const action = { type: 'UNKNOWN_ACTION' };
    
        expect(userReducer(initialState, action)).toEqual(initialState);
      });
    });

    В этих тестах проверяется, что при отправке action с типом SET_USER reducer корректно обновляет состояние. Также проверяется, что если action неизвестен, то состояние остается неизменным.

  2. Тестирование сложных reducers с несколькими action types В более сложных reducers может быть несколько action типов, которые изменяют различные части состояния. В таких случаях важно протестировать каждый из этих action типов.

    Пример сложного reducer:

    const initialState = {
      user: null,
      loading: false,
      error: null
    };
    
    const userReducer = (state = initialState, action) => {
      switch (action.type) {
        case 'SET_USER':
          return { ...state, user: action.payload };
        case 'SET_LOADING':
          return { ...state, loading: action.payload };
        case 'SET_ERROR':
          return { ...state, error: action.payload };
        default:
          return state;
      }
    };

    Тестирование данного reducer может выглядеть следующим образом:

    describe('userReducer with multiple actions', () => {
      it('should handle SET_USER action', () => {
        const action = { type: 'SET_USER', payload: { name: 'John' } };
        const expectedState = { user: { name: 'John' }, loading: false, error: null };
        expect(userReducer(undefined, action)).toEqual(expectedState);
      });
    
      it('should handle SET_LOADING action', () => {
        const action = { type: 'SET_LOADING', payload: true };
        const expectedState = { user: null, loading: true, error: null };
        expect(userReducer(undefined, action)).toEqual(expectedState);
      });
    
      it('should handle SET_ERROR action', () => {
        const action = { type: 'SET_ERROR', payload: 'Error occurred' };
        const expectedState = { user: null, loading: false, error: 'Error occurred' };
        expect(userReducer(undefined, action)).toEqual(expectedState);
      });
    });

    В данном примере проверяется, что reducer корректно обрабатывает различные action типы и изменяет состояние в зависимости от них.

Рекомендации по тестированию Actions и Reducers

  • Тестирование на разных уровнях. Тестировать только actions или только reducers — недостаточно. Рекомендуется писать тесты как для отдельных действий (actions), так и для работы reducer, чтобы убедиться в согласованности между этими частями приложения.

  • Использование mock-данных. При тестировании асинхронных actions важно использовать mock-данные и mock-функции для имитации API-запросов и других асинхронных операций.

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

  • Использование утверждений на каждом шаге. При тестировании важно не только проверять итоговый результат, но и утверждать, что каждый промежуточный шаг выполнен корректно.

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