如何测试仅调度其他操作的 Redux 操作创建器 [英] How do I test a Redux action creator that only dispatches other actions

查看:43
本文介绍了如何测试仅调度其他操作的 Redux 操作创建器的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

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

I'm having trouble testing an action creator that just loops through the array passed to it and dispatches an action for each item in that array. It's simple enough I just can't seem to figure it out. Here's the action creator:

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

这是我尝试测试它的方式:

And here's how I'm attempting to test it:

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:无法读取未定义的属性then".

推荐答案

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

序言

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

This example uses Axios which is a promise based library for making HTTP requests. However you can run this example using a different promise based request library such as Fetch. Alternatively just wrap a normal http request in a promise.

本示例中将使用 Mocha 和 Chai 进行测试.

Mocha and Chai will be used in this example for testing.

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

来自 redux 文档:

From the redux docs:

在调用异步 API 时,有两个关键时刻时间:开始通话的那一刻,接到电话的那一刻一个答案(或超时).

When you call an asynchronous API, there are two crucial moments in time: the moment you start the call, and the moment when you receive an answer (or a timeout).

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

We first need to define actions and their creators that are associated with making an asynchronous call to an external resource for any given topic id.

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

There are three possible states of a promise which represents an API request:

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

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

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

Okay lets write the core action creators we will need to represent the statefulness of a request for a given topic 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 }
}

请注意,您的减速器应该适当地处理这些操作.

Note that your reducers should handle these actions appropriately.

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

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

Axios is a promise based request library. So the axios.get method makes a request to the given url and returns a promise that will be resolved if successful otherwise this promise will be rejected

 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 操作,并带有来自我们的请求(我们的主题数据)的响应

If our Axios request is successful our promise will be resolved and the code in .then will be executed. This will dispatch a FETCH_FULFILLED action for our given topic id with a the response from our request (our topic data)

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

If the Axios request is unsuccessful our code in .catch will be executed and dispatch a FETCH_REJECTED action which will contain the topic ID and the error which occurred during the request.

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

Now we need to create a single action creator to that will start the fetching process for multiple topicIds.

由于这是一个异步过程,我们可以使用thunk action creator,它将使用 Redux-thunk 中间件来允许我们在未来分派额外的异步操作.

Since this is an asynchronous process we can use a thunk action creator that will use Redux-thunk middleware to allow us to dispatch additional async actions in the future.

Thunk Action 创建者如何工作?

我们的 thunk 动作创建者分派与为 多个 topicId 进行提取相关的动作.

Our thunk action creator dispatches actions associated with making fetches for multiple topicIds.

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

This single thunk action creator is an action creator that will be handled by our redux thunk middleware since it fits the signature associated with thunk action creators, that is it returns a function.

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

When store.dispatch is called our actions will go through the middleware chain before they reach the store. Redux Thunk is a piece of middleware that will see our action is a function and then give this function access to the stores dispatch and get state.

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

Here is the code inside Redux thunk that does this:

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

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

Okay so that is why our thunk action creator returns a function. because this function will be called by middleware and give us access to dispatch and get state meaning we can dispatch further actions at a later date.

编写我们的 thunk 动作创建器

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

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

    return Promise.all(itemPromisesArray) 
  };
};

最后我们返回一个对 promise.all 的调用.

At the end we return a call to promise.all.

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

This means that our thunk action creator returns one promise which waits for all our sub promises which represent individual fetches to be fulfilled (request success) or for the first rejection (request failure)

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

See it returns a function that accepts dispatch. This returned function is the function which will be called inside the Redux thunk middleware, therefore inverting control and letting us dispatch more actions after our fetches to external resources are made.

旁白 - 在我们的 thunk 动作创建器中访问 getState

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

As we saw in the previous function redux-thunk calls the function returned by our action creator with dispatch and getState.

我们可以像这样在我们的 thunk 动作创建者返回的函数中将其定义为一个 arg

We could define this as an arg inside the function returned by our thunk action creator like so

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 开始,因为这是最简单的解决方案.

Remember redux-thunk is not the only solution. if we wanted to dispatch promises instead of functions we could use redux-promise. However I would recommend starting with redux-thunk as this is the simplest solution.

测试我们的 thunk 动作创建器

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

So the test for our thunk action creator will comprise of the following steps:

  1. 创建一个模拟商店.
  2. 派遣 thunk 动作创建者3.确保针对以数组形式传递给 thunk 操作创建者的每个主题 id 完成所有异步获取之后,已调度 FETCH_PENDING 操作.
  1. create a mock store.
  2. dispatch the thunk action creator 3.Ensure that after all the async fetches complete for every topic id that was passed in an array to the thunk action creator a FETCH_PENDING action has been dispatched.

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

However we need to do two other sub steps we need to carry out in order to create this test:

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

拦截HTTP请求

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

We want to test that the correct number of a certain action are dispatched by a single call to the fetchAllItems action creator.

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

Okay now in the test we don't want to actually make a request to a given api. Remember our unit tests must be fast and deterministic. For a given set of arguments to our thunk action creator our test must always either fail or pass. If we actually fetched data from a server inside our tests then it may pass once and then fail if the server goes down.

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

Two possible ways of mocking the response from the server

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

  1. Mock the Axios.get function so that it returns a promise that we can force to resolve with the data we want or reject with our predefined error.

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

Use an HTTP mocking library like Nock which will let the Axios library make a request. However this HTTP request will be intercepted and handled by Nock instead of a real server. By using Nock we can specify the response for a given request within our tests.

我们的测试将从:

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并以确定的方式响应状态代码和响应.

Nock intercepts any HTTP request made to a url starting with http://www.example.com and responds in a deterministic manner with the status code and response.

创建我们的 Mock Redux 商店

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

In the test file import the configure store function from the redux-mock-store library to create our fake store.

import configureStore from 'redux-mock-store';

此模拟存储将在数组中分派的操作用于您的测试.

This mock store will the dispatched actions in an array to be used in your tests.

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

Since we are testing a thunk action creator our mock store needs to be configured with the redux-thunk middleware in our test

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

Out mock store 有一个 store.getActions 方法,当调用该方法时,它会为我们提供一个包含所有先前分派操作的数组.

Out mock store has a store.getActions method which when called gives us an array of all previously dispatched actions.

最后,我们调度 thunk 操作创建者,它返回一个承诺,当所有单独的 topicId 获取承诺都解决时,该承诺就会解决.

Finally we dispatch the thunk action creator which returns a promise which resolves when all of the individual topicId fetch promise are resolved.

然后,我们进行测试断言,以比较发送到模拟商店的实际操作与我们的预期操作.

We then make our test assertions to compare the actual actions that were to dispatched to the mock store versus our expected actions.

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

所以在测试结束时,我们将我们的 thunk 动作创建者分派到模拟商店.我们一定不要忘记返回这个调度调用,这样当 thunk 操作创建者返回的承诺被解析时,断言将在 .then 块中运行.

So at the end of the test we dispatch our thunk action creator to the mock store. We must not forget to return this dispatch call so that the assertions will be run in the .then block when the promise returned by the thunk action creator is resolved.

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

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

See the final test file below:

最终测试文件

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 操作创建器的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆