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

Posted

技术标签:

【中文标题】如何测试仅调度其他操作的 Redux 操作创建器【英文标题】:How do I test a Redux action creator that only dispatches other actions 【发布时间】:2017-05-22 09:39:17 【问题描述】:

我在测试一个动作创建器时遇到了麻烦,它只是循环传递给它的数组并为该数组中的每个项目分派一个动作。这很简单,我似乎无法弄清楚。这是动作创建者:

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

【问题讨论】:

您收到该错误是因为您没有在fetchAllItems 返回的函数中返回任何内容。 .forEach 也不返回任何东西。至于测试,你可能不得不使用 Rewire 或类似的东西来模拟fetchItems(我对此有点生疏,抱歉)。 @DonovanM 是正确的,你没有返回任何东西。您也可以将topicIds 映射到一组promise,然后使用Promise.all() 进行解析。 @OB3 是否可以模拟 dispatchfetchItem 并将这些模拟版本(可能作为间谍)传递给 fetchItems?可能是这样的:fetchAllItems([1,2])(mockDispatch, mockFetchItems)?谢谢。 【参考方案1】:

编写和测试向 API 发出基于 Promise 请求的 Redux Thunk Action Creator 的指南

序言

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

本例将使用 Mocha 和 Chai 进行测试。

用 Redux 操作表示请求的状态

来自 redux 文档:

调用异步 API 时,有两个关键时刻 时间:您开始通话的那一刻,以及您接听的那一刻 一个答案(或超时)。

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

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

待处理 (已提出请求) 已完成 (请求成功) 拒绝请求失败 - 或超时)

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

好的,让我们编写我们需要的核心动作创建者来表示对给定主题 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 是一个基于 Promise 的请求库。因此 axios.get 方法向给定的 url 发出请求并返回一个承诺,如果成功,该承诺将被解决,否则该承诺将被拒绝

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

如果我们的 Axios 请求成功,我们的 Promise 将被解决,.then 中的代码将被执行。这将为我们给定的主题 ID 调度一个 FETCH_FULFILLED 操作,并带有来自我们请求的响应(我们的主题数据)

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

现在我们需要创建一个单独的动作创建者来启动多个 topicId 的获取过程。

由于这是一个异步过程,我们可以使用 thunk 动作创建器,它将使用 Redux-thunk 中间件允许我们在未来调度其他异步动作。

Think Action 创建者如何工作?

我们的 thunk 动作创建者调度与获取 多个 topicId 相关的动作。

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

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

这是 Redux thunk 中执行此操作的代码:

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

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

编写我们的 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 动作创建器中访问 getState

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

我们可以在我们的 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 不是唯一的解决方案。如果我们想分发 promise 而不是函数,我们可以使用 redux-promise。不过我建议从 redux-thunk 开始,因为这是最简单的解决方案。

测试我们的 thunk 动作创建器

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

    创建模拟商店。 调度 thunk 动作创建者 3.确保所有异步获取完成后,对于以数组形式传递给 thunk 动作创建者的每个主题 id,一个 FETCH_PENDING 动作已被调度。

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

    我们需要模拟 HTTP 响应,这样我们就不会向实时服务器发出真正的请求 我们还想创建一个模拟商店,让我们能够查看所有已调度的历史操作。

拦截 HTTP 请求

我们想要测试通过一次调用 fetchAllItems 操作创建者来分派正确数量的某个操作。

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

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

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

    使用像 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 动作创建者,它返回一个承诺,当所有单独的 topicId 获取承诺都解决时,该承诺就会解决。

然后,我们进行测试断言,以比较要分派到模拟商店的实际操作与我们预期的操作。

在 Mocha 中测试我们的 thunk 动作创建者返回的承诺

因此,在测试结束时,我们将 thunk 动作创建者发送到模拟存储。我们不能忘记返回这个调度调用,这样当 thunk 动作创建者返回的承诺被解决时,断言将在 .then 块中运行。

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

请看下面的最终测试文件:

最终测试文件

test/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);
            );
    );
);

Final Action 创建者和辅助函数

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
  ;
;

【讨论】:

以上是关于如何测试仅调度其他操作的 Redux 操作创建器的主要内容,如果未能解决你的问题,请参考以下文章

jest redux-thunk 测试是不是调度了相同模块的操作

在 redux 中在哪里调度多个操作?

无法在 Typescript 中调度 redux 操作

使用 Jest 在 React Redux 中对多个调度的操作进行单元测试

使用 React-Router 和 Redux 时单击链接时如何调度操作?

Redux 中间件未调度新操作