如何测试仅调度其他操作的 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 是否可以模拟 dispatch
和 fetchItem
并将这些模拟版本(可能作为间谍)传递给 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 测试是不是调度了相同模块的操作
使用 Jest 在 React Redux 中对多个调度的操作进行单元测试