Почему я создал чай-редукс

Я использую 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. Он обновит состояние при запуске выборки данных и после ответа сервера.
В процессе получения данных выполняется ряд действий, в результате чего состояние обновляется несколько раз.

Этот процесс будет выглядеть так:

  1. Наш редуктор получает уведомление при запуске выборки данных.
  2. Наш редуктор получает уведомление об успешной выборке данных.
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.

Напишем тесты, которые…

  1. проверьте, правильно ли обновлены все состояния.
  2. убедитесь, что наше асинхронное действие ведет себя, как описано выше.
  3. опишите процесс.
  4. позволить другим разработчикам понять, как настроить хранилище 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);

… и мы проверили, как наше асинхронное действие работает должным образом.