Как стать автором
Поиск
Написать публикацию
Обновить

Тестирование redux store middleware

Уровень сложностиПростой
Время на прочтение3 мин
Количество просмотров1.2K

Бывает ситуация, когда нам необходимо протестировать middleware, либо асинхронное событие, которые возникают в хранилище redux.

Цель этой статьи в том, чтобы показать как тестировать action в redux store.

Есть готовое решение, redux-mock-store, но оно не позволяет оперировать реальным хранилищем, через него мы можем только проверить был вызван тот или иной action, а данные которые сохраняем мы в store, не можем проверить.

Я предлагаю не создавать fake store, а просто добавить свой middleware, который будет сохранять action, которые были вызваны. В последствии вы сможете проверять, был ли вызван он или нет. А также аргументы, с которыми он был вызван.

Скачать сам проект можно здесь, чтобы не писать самому.

Если в кратце, мы протестируем вызов ф-ции задания имени животному.

src/store/animal.ts - то, что будем тестировать.

import { createSlice, PayloadAction } from '@reduxjs/toolkit'

const initialState = {
    name: ''
}

const animalSlice = createSlice({
  name: 'animal',
  initialState,
  reducers: {
    setName: (state, action: PayloadAction<string>) => {
      state.name = action.payload;
    },
  },
})

export const { setName } = animalSlice.actions
export default animalSlice.reducer

src/tests/actionsMiddleware.ts - то, что нам позволяет хранить вызванные действия.

import { Middleware, Action, isAction } from "redux";

export interface ActionsMiddleware extends Middleware {
  getActions: () => Promise<Action[]>;
  clearActions: () => Promise<void>;
}

export const actionsMiddleware = () => {
  const actions: Action[] = [];

  const middleware: ActionsMiddleware = () => (next) => (action) => {
    if (isAction(action)) {
      actions.push(action);
    }
    return next(action);
  };

  middleware.getActions = async () => {
    return actions;
  };

  middleware.clearActions = async () => {
    actions.length = 0;
  };

  return middleware;
};

getActions сделан асинхронным, т.к. при работе с middleware нам нужно удостоверится что асинхронные действия поставленные в очередь redux, выполнились, т.е. по сути мы ждем следующую очередь, чтобы извлечь текущие actions.

src/tests/animal.test.ts - сам тест, в котором используем написанный middleware

import { configureStore } from "@reduxjs/toolkit";
import { ActionsMiddleware, actionsMiddleware } from "./actionsMiddleware";
import animalReducer, { setName } from "../store/animal";

function createStore(middleware: ActionsMiddleware) {
  return configureStore({
    reducer: {
      animal: animalReducer,
    },
    middleware: (getDefaultMiddleware) =>
      getDefaultMiddleware().concat(middleware),
  });
}

describe("animal test", () => {
  let store: ReturnType<typeof createStore>;
  let middleware: ActionsMiddleware;

  beforeEach(() => {
    middleware = actionsMiddleware();
    store = createStore(middleware);
  });

  afterEach(async () => {
    await middleware.clearActions();
  });

  it("saves only when conditions are met", async () => {
    store.dispatch(setName("cat"));

    let actions = await middleware.getActions();
    expect(actions).toHaveElementInArray(setName("cat"));
    expect(store.getState().animal.name).toBe("cat");

    await middleware.clearActions();

    actions = await middleware.getActions();
    expect(actions).not.toHaveElementInArray(setName("cat"));
    expect(store.getState().animal.name).toBe("cat");
  });
});

Как можно увидеть, для создания store в отдельную ф-цию вынес, чтобы можно было получить тип и им оперировать на 33 строке, иначе typescript тип неизвестен. Ну и такое решение гибкое, т.к. в зависимости от reducer у нас разные данные хранятся в хранилище.

У нас есть два действия:

getActions - это получить действия, которые были вызваны с момента создания store.

clearActions - утилитарный ф-ция, которая позволяет очистить стек вызовов действий.

Это демонстрация того, что redux можно и нужно тестировать.

И на последок ф-ция, которая используется для проверки toHaveElementInArray, есть ли объект внутри массива. Т.к. у нас стек, нужно убедиться, что было вызвано действие.

Я добавил это в файл setupTests.ts

expect.extend({
  toHaveElementInArray(received: any[], element: any) {
    const pass = received.some(
      (item) => JSON.stringify(item) === JSON.stringify(element)
    );

    if (pass) {
      return {
        message: () =>
          `expected ${JSON.stringify(received)} not to contain ${JSON.stringify(element)}`,
        pass: true,
      };
    } else {
      return {
        message: () =>
          `expected ${JSON.stringify(received)} to contain ${JSON.stringify(element)}`,
        pass: false,
      };
    }
  },
});

Для типизации создал файл src/test.d.ts:

declare global {
  namespace jest {
    interface Matchers<R> {
      toHaveElementInArray(element: any): R;
    }
  }
}

Делитесь своими советами, как вы тестируете redux.

UPD 19.10.2024: Оказывается есть метод toContainEqual, поэтому нет необходимости в создании собственного метода toHaveElementInArray.

Только зарегистрированные пользователи могут участвовать в опросе. Войдите, пожалуйста.
А вы тестируете redux?
0% Полностью0
0% Частично, не все кейсы умеем покрывать (или не нужно)0
100% У нас нет тестов (мы не тестируем redux)5
Проголосовали 5 пользователей. Воздержались 4 пользователя.
Теги:
Хабы:
Всего голосов 4: ↑2 и ↓2+4
Комментарии8

Публикации

Ближайшие события