Redux — это одна из самых популярных библиотек для управления состоянием в JavaScript-приложениях, особенно в связке с React. Тестирование Redux-приложений важно для обеспечения правильной работы логики и предсказуемости состояний. В этой главе рассмотрим, как эффективно тестировать reducer-ы с использованием React Testing Library и других инструментов для тестирования.
Reducer — это чистая функция, которая принимает два аргумента: текущее состояние и действие, и возвращает новое состояние. В Redux reducer-ы используются для обработки всех изменений состояния приложения. Их задача — принять определённое действие (action) и изменить состояние, соответствующим образом реагируя на это действие.
Пример простого reducer-а:
const initialState = { count: 0 };
function counterReducer(state = initialState, action) {
switch (action.type) {
case 'INCREMENT':
return { count: state.count + 1 };
case 'DECREMENT':
return { count: state.count - 1 };
default:
return state;
}
}
Этот reducer обрабатывает два типа действий: INCREMENT и
DECREMENT, изменяя свойство count в
состоянии.
Тестирование reducer-ов отличается от тестирования React-компонентов. Основная цель заключается в том, чтобы проверить, правильно ли работает логика изменения состояния на основе поступающих действий. Тесты должны быть быстрыми и независимыми, так как reducer-ы — это чистые функции без побочных эффектов.
Тестирование reducer-а обычно включает следующие шаги:
Для начала создадим тесты для приведённого выше
counterReducer.
import { counterReducer } from './counterReducer';
describe('counterReducer', () => {
it('должен вернуть начальное состояние при отсутствии переданного состояния', () => {
const initialState = { count: 0 };
expect(counterReducer(undefined, {})).toEqual(initialState);
});
it('должен увеличить счетчик при действии INCREMENT', () => {
const action = { type: 'INCREMENT' };
const initialState = { count: 0 };
const expectedState = { count: 1 };
expect(counterReducer(initialState, action)).toEqual(expectedState);
});
it('должен уменьшить счетчик при действии DECREMENT', () => {
const action = { type: 'DECREMENT' };
const initialState = { count: 1 };
const expectedState = { count: 0 };
expect(counterReducer(initialState, action)).toEqual(expectedState);
});
it('должен возвращать текущее состояние для неизвестных действий', () => {
const action = { type: 'UNKNOWN' };
const initialState = { count: 0 };
expect(counterReducer(initialState, action)).toEqual(initialState);
});
});
Здесь используется фреймворк тестирования Jest. В этих
тестах:
INCREMENT и DECREMENT.В большинстве современных приложений с использованием Redux часто
применяются асинхронные операции, такие как запросы к серверу. Чтобы
тестировать такие операции, часто используются middleware, такие как
redux-thunk. Важно понимать, что тестирование reducer-ов,
которые не занимаются асинхронной логикой, остаётся таким же, как
показано выше.
Для асинхронных действий тестирование обычно будет происходить на уровне действий или компонентов, а не самих reducer-ов. Однако иногда стоит проверить, как reducer реагирует на действия, генерируемые асинхронными операциями.
Пример асинхронного действия с использованием
redux-thunk:
const fetchData = () => {
return dispatch => {
dispatch({ type: 'FETCH_REQUEST' });
return fetch('/data')
.then(response => response.json())
.then(data => {
dispatch({ type: 'FETCH_SUCCESS', payload: data });
})
.catch(error => {
dispatch({ type: 'FETCH_FAILURE', error });
});
};
};
Этот код не относится напрямую к reducer-у, однако reducer может
обрабатывать такие действия, как FETCH_REQUEST,
FETCH_SUCCESS и FETCH_FAILURE. Тестирование
асинхронных операций в Redux обычно включает проверку, как состояние
меняется после выполнения этих действий.
Для тестирования асинхронных действий в Redux можно использовать
такие библиотеки, как redux-mock-store и
jest-fetch-mock.
Пример теста с использованием redux-mock-store:
import configureMockStore from 'redux-mock-store';
import thunk from 'redux-thunk';
import { fetchData } from './actions';
const middlewares = [thunk];
const mockStore = configureMockStore(middlewares);
describe('fetchData', () => {
it('должен выполнять асинхронный запрос и вызывать соответствующие действия', () => {
const store = mockStore({});
const mockData = { name: 'Test' };
jest.spyOn(global, 'fetch').mockResolvedValue({
json: jest.fn().mockResolvedValue(mockData),
});
const expectedActions = [
{ type: 'FETCH_REQUEST' },
{ type: 'FETCH_SUCCESS', payload: mockData },
];
return store.dispatch(fetchData()).then(() => {
expect(store.getActions()).toEqual(expectedActions);
global.fetch.mockRestore();
});
});
});
В этом тесте:
fetch.fetchData
вызываются ожидаемые действия (FETCH_REQUEST и
FETCH_SUCCESS).Когда приложение становится большим, а reducer-ы растут, важно поддерживать структуру тестов в порядке. Хорошей практикой является организация тестов для каждого reducer-а отдельно, а также группировка тестов для связанных reducer-ов в единые пакеты.
Например, если в приложении используется несколько различных reducer-ов для управления состоянием пользователя, товаров и корзины, можно разделить тесты на несколько файлов:
userReducer.test.jsproductReducer.test.jscartReducer.test.jsТакже стоит соблюдать принцип изоляции тестов, чтобы каждый тест проверял только одну конкретную функцию. Это позволяет выявлять ошибки на ранних этапах и облегчает поддержку тестов.
Тестирование reducer-ов в Redux — это важный этап обеспечения
качества приложения. Основной акцент следует делать на проверке
правильности обработки различных действий и неизменности состояния при
непредвиденных ситуациях. Правильная организация тестов и использование
инструментов, таких как jest,
redux-mock-store, поможет убедиться, что ваше приложение
работает так, как ожидалось.