简体   繁体   English

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

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

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...
      });
  });
});

I'm getting this error: TypeError: Cannot read property 'then' of undefined . 我收到此错误: TypeError: Cannot read property 'then' of undefined

A Guide to Writing and Testing Redux Thunk Action Creators that make a Promise Based Request to an API 编写和测试Redux Thunk Action Creators的指南,它为API提供基于Promise的请求

Preamble 前言

This example uses Axios which is a promise based library for making HTTP requests. 此示例使用Axios ,它是一个基于promise的库,用于发出HTTP请求。 However you can run this example using a different promise based request library such as Fetch . 但是,您可以使用不同的基于承诺的请求库(如Fetch)来运行此示例。 Alternatively just wrap a normal http request in a promise. 或者只是在promise中包含一个普通的http请求。

Mocha and Chai will be used in this example for testing. Mocha和Chai将在此示例中用于测试。

Representing the statefulness of a request with Redux actions 使用Redux操作表示请求的有状态

From the redux docs: 来自redux文档:

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). 当您调用异步API时,有两个关键时刻:您开始呼叫的那一刻,以及您收到答案(或超时)的那一刻。

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

There are three possible states of a promise which represents an API request: 承诺有三种可能的状态代表API请求:

  • Pending (request made) 待定 (请求)
  • Fulfilled (request successful) 完成 (要求成功)
  • Rejected ( request failed - or timeout) 被拒绝请求失败 - 或超时)

Core Action Creators which represent state of request promise 核心动作创建者,代表请求承诺的状态

Okay lets write the core action creators we will need to represent the statefulness of a request for a given topic id. 好吧,让我们编写核心动作创建者,我们将需要表示给定主题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. 请注意,您的Reducer应该适当地处理这些操作。

Logic for a single fetch action creator 单个获取操作创建者的逻辑

Axios is a promise based request library. Axios是一个基于承诺的请求库。 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 所以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))
              }) 
}

If our Axios request is successful our promise will be resolved and the code in .then will be executed . 如果我们的Axios请求成功,我们的承诺将得到解决,并且.then中的代码将被执行 This will dispatch a FETCH_FULFILLED action for our given topic id with a the response from our request (our topic data) 这将根据我们的请求(我们的主题数据)为我们的给定主题id调度FETCH_FULFILLED操作

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

Now we need to create a single action creator to that will start the fetching process for multiple topicIds. 现在我们需要创建一个单独的动作创建器,以便为多个topicId启动提取过程。

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动作创建器 ,它将使用Redux-thunk中间件来允许我们在将来发送额外的异步动作。

How does a Thunk Action creator work? Thunk Action的创建者如何运作?

Our thunk action creator dispatches actions associated with making fetches for multiple topicIds. 我们的thunk action creator会调度与多个 topicId的提取相关的操作。

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

When store.dispatch is called our actions will go through the middleware chain before they reach the store. 当调用store.dispatch时,我们的操作将在到达商店之前通过中间件链。 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: 这是Redux thunk里面的代码:

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

Okay so that is why our thunk action creator returns a function. 好的,这就是为什么我们的thunk action creator返回一个函数。 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. 因为这个函数将被中间件调用并让我们访问调度和获取状态,这意味着我们可以在以后调度进一步的操作。

Writing our thunk action creator 写我们的thunk动作创作者

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

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

    return Promise.all(itemPromisesArray) 
  };
};

At the end we return a call to promise.all. 最后,我们返回对promise.all的调用。

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) 这意味着我们的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. 这个返回的函数是将在Redux thunk中间件内部调用的函数,因此反转控制并让我们在获取外部资源后调度更多动作。

An aside - accessing getState in our thunk action creator 另外 - 在我们的thunk action creator中访问getState

As we saw in the previous function redux-thunk calls the function returned by our action creator with dispatch and getState. 正如我们在前面的函数中看到的,redux-thunk使用dispatch和getState调用我们的动作创建者返回的函数。

We could define this as an arg inside the function returned by our thunk action creator like so 我们可以将此定义为我们的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)
  };
};

Remember redux-thunk is not the only solution. 记住redux-thunk不是唯一的解决方案。 if we wanted to dispatch promises instead of functions we could use redux-promise. 如果我们想要派遣承诺而不是函数,我们可以使用redux-promise。 However I would recommend starting with redux-thunk as this is the simplest solution. 但是我建议从redux-thunk开始,因为这是最简单的解决方案。

Testing our thunk action creator 测试我们的thunk动作创建者

So the test for our thunk action creator will comprise of the following steps: 因此,对我们的thunk动作创建者的测试将包括以下步骤:

  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. 派遣的thunk行动的创建者3.确保毕竟异步获取完整的针对在一个阵列形实转换动作创造者FETCH_PENDING动作已分发传递的每个主题ID。

However we need to do two other sub steps we need to carry out in order to create this test: 但是,为了创建此测试,我们需要执行另外两个子步骤:

  1. We need to mock HTTP responses so we don't make real requests to a live Server 我们需要模拟HTTP响应,因此我们不会向实时服务器发出实际请求
  2. we also want to create a mock store that allows us to see all the historical actions that have been dispatched. 我们还想创建一个模拟商店,允许我们查看已分派的所有历史操作。

Intercepting the HTTP request 拦截HTTP请求

We want to test that the correct number of a certain action are dispatched by a single call to the fetchAllItems action creator. 我们想测试一次调用fetchAllItems动作创建者调度某个动作的正确数量。

Okay now in the test we don't want to actually make a request to a given api. 好的,现在在测试中我们不想实际向给定的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. 对于我们的thunk动作创建者的给定参数集,我们的测试必须始终失败或通过。 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. 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. 模拟Axios.get函数,以便它返回一个promise,我们可以使用我们预定义的错误强制解析我们想要或拒绝的数据。

  2. Use an HTTP mocking library like Nock which will let the Axios library make a request. 使用像Nock这样的HTTP模拟库,它将让Axios库发出请求。 However this HTTP request will be intercepted and handled by Nock instead of a real server. 但是,这个HTTP请求将由Nock而不是真实服务器拦截和处理。 By using Nock we can specify the response for a given request within our tests. 通过使用Nock,我们可以在测试中指定给定请求的响应。

Our test will start with: 我们的测试将从以下开始:

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

Creating our Mock Redux store 创建我们的Mock Redux商店

In the test file import the configure store function from the redux-mock-store library to create our fake store. 在测试文件中,从redux-mock-store库导入configure store函数以创建我们的假存储。

import configureStore from 'redux-mock-store';

This mock store will the dispatched actions in an array to be used in your tests. 此模拟存储将在数组中调度的操作将在您的测试中使用。

Since we are testing a thunk action creator our mock store needs to be configured with the redux-thunk middleware in our test 由于我们正在测试一个thunk动作创建器,我们的模拟存储需要在我们的测试中配置redux-thunk中间件

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

Out mock store has a store.getActions method which when called gives us an array of all previously dispatched actions. Out mock store有一个store.getActions方法,当调用它时会给我们一个包含所有以前调度的动作的数组。

Finally we dispatch the thunk action creator which returns a promise which resolves when all of the individual topicId fetch promise are resolved. 最后,我们调度thunk动作创建器,该动作创建器返回一个promise,该promise在解析所有单个topicId fetch promise时解析。

We then make our test assertions to compare the actual actions that were to dispatched to the mock store versus our expected actions. 然后,我们进行测试断言,以比较调度到模拟存储的实际操作与我们预期的操作。

Testing the promise returned by our thunk action creator in Mocha 测试我们的摩卡动作创建者返回的承诺

So at the end of the test we dispatch our thunk action creator to the mock store. 因此,在测试结束时,我们将thunk动作创建者发送到模拟商店。 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. 我们不能忘记返回此调度调用,以便在thunk动作创建者返回的promise被解析时,断言将在.then块中运行。

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

See the final test file below: 请参阅下面的最终测试文件:

Final test file 最终的测试文件

test/index.js 测试/ 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 creators and helper functions 最终动作创建者和辅助函数

src/index.js 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.

相关问题 如何在redux操作(或操作中分配的其他回调)的上下文中使用jest测试`image.onload` - How do I test `image.onload` using jest in the context of redux actions (or other callbacks assigned in the action) 如何测试调度 Redux / Thunk 操作的 React 组件 - How to test a React component that dispatches a Redux / Thunk action 如何改善这个Redux动作创建者? - How can i improve this redux action creator? 仅在触发其他两个操作时运行Redux操作 - Run Redux action only when two other actions were triggered 测试分派多个动作创建者的动作创建者 - Testing an action creator that dispatches multiple action creators 如何测试使用setTimeout调用另一个操作的异步操作创建器 - How do I test an async action creator that calls another action with setTimeout 如何输入Redux重击动作创建者以返回承诺 - How can I type a Redux thunk action creator to return a promise React Redux-我的动作创建者未将动作传递给减速器(同步) - React Redux- My action creator is not passing actions to reducer (sync) 您如何在React Redux容器中自定义调度? - How do you customize dispatches in React Redux containers? onClick 调度一个动作。 我如何等待我的派遣完成? - onClick dispatches an action. How do i wait for my dispatch to complete?
 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM