Бывает ситуация, когда нам необходимо протестировать 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.
