繁体   English   中英

如何测试仅调度其他操作的Redux操作创建者

[英]How do I test a Redux action creator that only dispatches other actions

我在测试一个动作创建器时遇到了麻烦,该动作创建器只是遍历传递给它的数组,并为该数组中的每个项目调度一个动作。 这很简单,我似乎无法弄明白。 这是动作创建者:

export const fetchAllItems = (topicIds)=>{
  return (dispatch)=>{
    topicIds.forEach((topicId)=>{
      dispatch(fetchItems(topicId));
    });
  };
};

以下是我试图测试的方法:

describe('fetchAllItems', ()=>{
  it('should dispatch fetchItems actions for each topic id passed to it', ()=>{
    const store = mockStore({});
    return store.dispatch(fetchAllItems(['1']))
      .then(()=>{
        const actions = store.getActions();
        console.log(actions);
        //expect... I can figure this out once `actions` returns...
      });
  });
});

我收到此错误: TypeError: Cannot read property 'then' of undefined

编写和测试Redux Thunk Action Creators的指南,它为API提供基于Promise的请求

前言

此示例使用Axios ,它是一个基于promise的库,用于发出HTTP请求。 但是,您可以使用不同的基于承诺的请求库(如Fetch)来运行此示例。 或者只是在promise中包含一个普通的http请求。

Mocha和Chai将在此示例中用于测试。

使用Redux操作表示请求的有状态

来自redux文档:

当您调用异步API时,有两个关键时刻:您开始呼叫的那一刻,以及您收到答案(或超时)的那一刻。

我们首先需要定义与为任何给定主题id进行异步调用外部资源相关联的操作及其创建者。

承诺有三种可能的状态代表API请求:

  • 待定 (请求)
  • 完成 (要求成功)
  • 被拒绝请求失败 - 或超时)

核心动作创建者,代表请求承诺的状态

好吧,让我们编写核心动作创建者,我们将需要表示给定主题ID的请求的有状态。

const fetchPending = (topicId) => {
  return { type: 'FETCH_PENDING', topicId }
}

const fetchFulfilled = (topicId, response) => { 
  return { type: 'FETCH_FULFILLED', topicId, response }
}

const fetchRejected = (topicId, err) => {
  return { type: 'FETCH_REJECTED', topicId, err }
}

请注意,您的Reducer应该适当地处理这些操作。

单个获取操作创建者的逻辑

Axios是一个基于承诺的请求库。 所以axios.get方法向给定的url发出请求并返回一个promise,如果成功则将被解析,否则这个promise将被拒绝

 const makeAPromiseAndHandleResponse = (topicId, url, dispatch) => {
 return axios.get(url)
              .then(response => {
                dispatch(fetchFulfilled(topicId, response))
              })
              .catch(err => {
                dispatch(fetchRejected(topicId, err))
              }) 
}

如果我们的Axios请求成功,我们的承诺将得到解决,并且.then中的代码将被执行 这将根据我们的请求(我们的主题数据)为我们的给定主题id调度FETCH_FULFILLED操作

如果Axios请求不成功,我们.catch中执行代码并调度FETCH_REJECTED操作,该操作将包含主题ID和请求期间发生的错误。

现在我们需要创建一个单独的动作创建器,以便为多个topicId启动提取过程。

由于这是一个异步过程,我们可以使用thunk动作创建器 ,它将使用Redux-thunk中间件来允许我们在将来发送额外的异步动作。

Thunk Action的创建者如何运作?

我们的thunk action creator会调度与多个 topicId的提取相关的操作。

这个单一的thunk动作创建者是一个动作创建者,它将由我们的redux thunk中间件处理,因为它适合与thunk动作创建者相关联的签名,即它返回一个函数。

当调用store.dispatch时,我们的操作将在到达商店之前通过中间件链。 Redux Thunk是一个中间件,它会看到我们的动作是一个函数,然后让这个函数访问商店调度和获取状态。

这是Redux thunk里面的代码:

if (typeof action === 'function') {
  return action(dispatch, getState, extraArgument);
}

好的,这就是为什么我们的thunk action creator返回一个函数。 因为这个函数将被中间件调用并让我们访问调度和获取状态,这意味着我们可以在以后调度进一步的操作。

写我们的thunk动作创作者

export const fetchAllItems = (topicIds, baseUrl) => {
    return dispatch => {

    const itemPromisesArray = topicIds.map(id => fetchItem(dispatch, id, baseUrl))  

    return Promise.all(itemPromisesArray) 
  };
};

最后,我们返回对promise.all的调用。

这意味着我们的thunk动作创建者返回一个等待所有子承诺的承诺,这些承诺代表要完成的单个提取(请求成功)或第一次拒绝(请求失败)

看到它返回一个接受调度的函数。 这个返回的函数是将在Redux thunk中间件内部调用的函数,因此反转控制并让我们在获取外部资源后调度更多动作。

另外 - 在我们的thunk action creator中访问getState

正如我们在前面的函数中看到的,redux-thunk使用dispatch和getState调用我们的动作创建者返回的函数。

我们可以将此定义为我们的thunk动作创建者返回的函数内的arg

export const fetchAllItems = (topicIds, baseUrl) => {
   return (dispatch, getState) => {

    /* Do something with getState */
    const itemPromisesArray = topicIds.map(id => fetchItem(dispatch, id, baseUrl))

    return Promise.all(itemPromisesArray)
  };
};

记住redux-thunk不是唯一的解决方案。 如果我们想要派遣承诺而不是函数,我们可以使用redux-promise。 但是我建议从redux-thunk开始,因为这是最简单的解决方案。

测试我们的thunk动作创建者

因此,对我们的thunk动作创建者的测试将包括以下步骤:

  1. 创建一个模拟商店。
  2. 派遣的thunk行动的创建者3.确保毕竟异步获取完整的针对在一个阵列形实转换动作创造者FETCH_PENDING动作已分发传递的每个主题ID。

但是,为了创建此测试,我们需要执行另外两个子步骤:

  1. 我们需要模拟HTTP响应,因此我们不会向实时服务器发出实际请求
  2. 我们还想创建一个模拟商店,允许我们查看已分派的所有历史操作。

拦截HTTP请求

我们想测试一次调用fetchAllItems动作创建者调度某个动作的正确数量。

好的,现在在测试中我们不想实际向给定的api发出请求。 请记住,我们的单元测试必须快速且确定。 对于我们的thunk动作创建者的给定参数集,我们的测试必须始终失败或通过。 如果我们实际从我们测试中的服务器获取数据,那么它可能会传递一次然后在服务器出现故障时失败。

从服务器模拟响应的两种可能方法

  1. 模拟Axios.get函数,以便它返回一个promise,我们可以使用我们预定义的错误强制解析我们想要或拒绝的数据。

  2. 使用像Nock这样的HTTP模拟库,它将让Axios库发出请求。 但是,这个HTTP请求将由Nock而不是真实服务器拦截和处理。 通过使用Nock,我们可以在测试中指定给定请求的响应。

我们的测试将从以下开始:

describe('fetchAllItems', () => {
  it('should dispatch fetchItems actions for each topic id passed to it', () => {
    const mockedUrl = "http://www.example.com";
    nock(mockedUrl)
        // ensure all urls starting with mocked url are intercepted
        .filteringPath(function(path) { 
            return '/';
          })
       .get("/")
       .reply(200, 'success!');

});

Nock拦截从http://www.example.com开始对URL进行的任何HTTP请求,并以确定的方式响应状态代码和响应。

创建我们的Mock Redux商店

在测试文件中,从redux-mock-store库导入configure store函数以创建我们的假存储。

import configureStore from 'redux-mock-store';

此模拟存储将在数组中调度的操作将在您的测试中使用。

由于我们正在测试一个thunk动作创建器,我们的模拟存储需要在我们的测试中配置redux-thunk中间件

const middlewares = [ReduxThunk];
const mockStore = configureStore(middlewares);

Out mock store有一个store.getActions方法,当调用它时会给我们一个包含所有以前调度的动作的数组。

最后,我们调度thunk动作创建器,该动作创建器返回一个promise,该promise在解析所有单个topicId fetch promise时解析。

然后,我们进行测试断言,以比较调度到模拟存储的实际操作与我们预期的操作。

测试我们的摩卡动作创建者返回的承诺

因此,在测试结束时,我们将thunk动作创建者发送到模拟商店。 我们不能忘记返回此调度调用,以便在thunk动作创建者返回的promise被解析时,断言将在.then块中运行。

  return store.dispatch(fetchAllItems(fakeTopicIds, mockedUrl))
              .then(() => {
                 const actionsLog = store.getActions();
                 expect(getPendingActionCount(actionsLog))
                        .to.equal(fakeTopicIds.length);
              });

请参阅下面的最终测试文件:

最终的测试文件

测试/ index.js

import configureStore from 'redux-mock-store';
import nock from 'nock';
import axios from 'axios';
import ReduxThunk from 'redux-thunk'
import { expect } from 'chai';

// replace this import
import { fetchAllItems } from '../src/index.js';


describe('fetchAllItems', () => {
    it('should dispatch fetchItems actions for each topic id passed to it', () => {
        const mockedUrl = "http://www.example.com";
        nock(mockedUrl)
            .filteringPath(function(path) {
                return '/';
            })
            .get("/")
            .reply(200, 'success!');

        const middlewares = [ReduxThunk];
        const mockStore = configureStore(middlewares);
        const store = mockStore({});
        const fakeTopicIds = ['1', '2', '3'];
        const getPendingActionCount = (actions) => actions.filter(e => e.type === 'FETCH_PENDING').length

        return store.dispatch(fetchAllItems(fakeTopicIds, mockedUrl))
            .then(() => {
                const actionsLog = store.getActions();
                expect(getPendingActionCount(actionsLog)).to.equal(fakeTopicIds.length);
            });
    });
});

最终动作创建者和辅助函数

SRC / index.js

// action creators
const fetchPending = (topicId) => {
  return { type: 'FETCH_PENDING', topicId }
}

const fetchFulfilled = (topicId, response) => { 
  return { type: 'FETCH_FULFILLED', topicId, response }
}

const fetchRejected = (topicId, err) => {
  return { type: 'FETCH_REJECTED', topicId, err }
}

const makeAPromiseAndHandleResponse = (topicId, url, dispatch) => {
 return axios.get(url)
              .then(response => {
                dispatch(fetchFulfilled(topicId, response))
              })
              .catch(err => {
                dispatch(fetchRejected(topicId, err))
              }) 
}

// fundamentally must return a promise
const fetchItem = (dispatch, topicId, baseUrl) => {
  const url = baseUrl + '/' + topicId // change this to map your topicId to url 
  dispatch(fetchPending(topicId))
  return makeAPromiseAndHandleResponse(topicId, url, dispatch);
}

export const fetchAllItems = (topicIds, baseUrl) => {
   return dispatch => {
    const itemPromisesArray = topicIds.map(id => fetchItem(dispatch, id, baseUrl))
    return Promise.all(itemPromisesArray) // return a promise that waits for all fulfillments or first rejection
  };
};

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM