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