Почему я создал чай-редукс
Я использую redux уже больше года - и я большая фанатка.
Мне нравится, что редуктор - это чистая функция. Что у него нет побочных эффектов. И мне нравится, что редуктор легко тестируется.
Но для меня тестирование редуктора - это только половина правды. Я тоже предпочитаю писать интеграционный тест, который охватывает:
- асинхронные действия с использованием промежуточного программного обеспечения
- создать резервный магазин, соединив все вместе.
Однако написание интеграционных тестов с асинхронными действиями всегда было для меня болезненным. Мне как-то всегда удавалось его протестировать, но часто ценой читабельности.
Вот почему я решил, что написание такого теста должно быть таким же простым, как:
const store = chai.createReduxStore({ reducer, middleware: thunk }); expect(store).to.have.eventually .dispatched('FETCH') .then.dispatched({ type: 'SUCCESSFUL', name}) .notify(done);
Но я забегаю вперед. Начнем с самого начала.
Давайте создадим магазин redux.
Мы создадим хранилище redux, которое будет асинхронно извлекать данные из API.
Наш магазин будет поддерживать четыре состояния:
- исходный
- загрузка
- успех
- отказ
И послушайте три действия:
- FETCH → установит состояние загрузки
- УСПЕШНО → установит состояние успеха
- FAILED → установит состояние отказа.
Начнем с редуктора.
Редуктор - это чистая функция. Может быть отправлено синхронное действие, которое немедленно обновляет состояние.
const INITIAL_STATE = {loading: false, loaded: false, error: false, name: null}; export default (state = INITIAL_STATE, action) => { switch (action.type) { case 'FETCH': return { ...INITIAL_STATE, loading: true }; case 'SUCCESSFUL': return { ...INITIAL_STATE, loaded: true, name: action.name }; case 'FAILED': return { ...INITIAL_STATE, error: true }; } return state; };
Этот редуктор будет обрабатывать выборку данных из API. Он обновит состояние при запуске выборки данных и после ответа сервера.
В процессе получения данных выполняется ряд действий, в результате чего состояние обновляется несколько раз.
Этот процесс будет выглядеть так:
- Наш редуктор получает уведомление при запуске выборки данных.
- Наш редуктор получает уведомление об успешной выборке данных.
it('should successfully fetch data from API', () => { store.dispatch({type: 'FETCH'}); expect(store.getState().loading).to.be.true; store.dispatch({ type: 'SUCCESSFUL', name: 'redux' }); expect(store.getState().loaded).to.be.true; expect(store.getState().details).to.eql({name: 'redux'}); });
Приведенный выше тест показывает, как будет выглядеть наш процесс / поток.
Мы реализуем этот поток, используя redux-thunk
промежуточное ПО и fetch
для асинхронной выборки данных. Наше действие отправляет действие синхронизации, когда оно запускается, и одно, когда получение данных было успешным или неудачным.
import 'isomorphic-fetch'; export const fetchData = () => (dispatch) => { dispatch({type: 'FETCH'}); fetch('https://registry.npmjs.org/redux/latest') .then(response => response.json()) .then(({name}) => { dispatch({type: 'SUCCESSFUL', name}); }) .catch(() => { dispatch({type: 'FAILED'}); }); };
Теперь мы можем настроить наш магазин. Соединяем все вместе.
import { createStore, applyMiddleware } from 'redux'; import reducer, { fetchData } from './reducer'; const globalStore = createStore(reducer, applyMiddleware(thunk)); globalStore.dispatch(fetchData());
Мы успешно внедрили все компоненты нашего магазина redux.
Напишем тесты, которые…
- проверьте, правильно ли обновлены все состояния.
- убедитесь, что наше асинхронное действие ведет себя, как описано выше.
- опишите процесс.
- позволить другим разработчикам понять, как настроить хранилище redux.
Для 1. мы создадим простые модульные тесты без chai-redux. Не нужно ничего изобретать заново. Тестирование обновления состояния - одно из преимуществ redux.
it('should have initial state', () => { const nextState = reducer(undefined, {type: '@@INIT'}); expect(nextState).to.eql({loading: false, loaded: false, error: false, name: null}); }); it('should set loading', () => { const nextState = reducer({loading: false}, {type: 'FETCH'}); expect(nextState.loading).to.be.true; }); it('should set loaded and details if action is SUCCESSFUL', () => { const nextState = reducer({loading: true}, { type: 'SUCCESSFUL', name: 'redux' }); expect(nextState.loaded).to.be.true; expect(nextState.name).to.eql('redux'); });
Для 2.–4. Мы будем использовать chai-redux.
тесты chai-redux
Пришло время настроить (chai-) redux store и написать больше тестов.
import chai, {expect} from 'chai'; import chaiRedux from 'chai-redux'; import reducer from './reducer'; chai.use(chaiRedux); // ... const store = chai.createReduxStore({reducer});
Хранилище chai-redux может быть настроено с редуктором (-ами) и, при желании, с начальным состоянием и промежуточным программным обеспечением (-ами). Плагин chai-redux включает следующие утверждения:
- .state (), .state.like ()
- .dispatched ()
- .eventually & .notify ()
- then.state (), then.dispatched ()
штат
проверит, есть ли в истории состояний магазина данное состояние.
Мы можем использовать его для проверки исходного состояния нашего магазина.
expect(store).to.have .state({loading: false, loaded: false, error: false, name: null}); // partial comparison expect(store).to.have.state.like({ loaded: false, name: null});
отправлен
Затем мы проверяем, что наше асинхронное действие ведет себя, как описано выше.
const store = chai.createReduxStore({reducer, middleware: thunk}); //when store.dispatch(fetchData()); //then expect(store).to.have.dispatched('FETCH');
Что здесь происходит:
- Мы настраиваем магазин с использованием промежуточного программного обеспечения redux-thunk.
- Мы отправляем наше асинхронное действие.
- Мы подтверждаем, что действие FETCH было отправлено.
Примечание. Мы только что описали, как нужно настроить наш редуктор, и это помогает другим разработчикам понять его.
в конечном итоге / уведомить
Однако наше асинхронное действие не отправит свои действия немедленно. .eventually
будет ждать отправки действия и .notify
chai.
// then expect(store).to.eventually.have .dispatched('FETCH') .notify(done);
тогда
Давайте добавим в наш тест успешное действие. Мы ожидаем, что он будет отправлен сразу после FETCH.
expect(store).to.eventually.have .dispatched('FETCH') .then.dispatched({type: 'SUCCESSFUL', name: 'redux'}) .notify(done);
Мы успешно протестировали наш магазин redux. Мы проверили, что все изменения состояния выполнены правильно…
const store = chai.createReduxStore({reducer, middleware: [thunk]}); // when store.dispatch(fetchData()); // then expect(store).to.eventually.have .dispatched('FETCH') .then.dispatched( {type: 'SUCCESSFUL', name: 'redux'}) .notify(done);
… и мы проверили, как наше асинхронное действие работает должным образом.