我无法测试一个动作创建器,它只是循环传递给它的数组,并为该数组中的每个项目调度一个动作。这很简单,我似乎无法弄明白。这是动作创建者:
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
。
答案 0 :(得分:9)
编写和测试对API做出基于承诺的请求的Redux Thunk Action Creator指南
序言
此示例使用Axios这是一个基于承诺的库,用于发出HTTP请求。但是,您可以使用不同的基于承诺的请求库(例如Fetch)来运行此示例。或者只是在promise中包含一个普通的http请求。
将在此示例中使用Mocha和Chai进行测试。
使用Redux操作表示请求的有状态
来自redux docs:
当您调用异步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 action creator ,它将使用Redux-thunk中间件来允许我们在将来调度其他异步操作。
Thunk Action创建者如何运作?
我们的thunk action creator会调度与多个 topicIds的提取相关联的操作。
这个单一的thunk动作创建者是一个动作创建者,它将由我们的redux thunk中间件处理,因为它适合与thunk动作创建者相关联的签名,即它返回一个函数。
当调用store.dispatch时,我们的操作将在到达商店之前通过中间件链。 Redux Thunk是一个中间件,它会看到我们的操作是一个函数,然后让这个函数访问商店调度和获取状态。
这是Redux thunk里面的代码:
if (typeof action === 'function') {
return action(dispatch, getState, extraArgument);
}
好的,这就是我们的thunk action creator返回一个函数的原因。因为这个函数将被中间件调用,并允许我们访问调度和获取状态,这意味着我们可以在以后调度进一步的操作。
编写我们的thunk action creator
export const fetchAllItems = (topicIds, baseUrl) => {
return dispatch => {
const itemPromisesArray = topicIds.map(id => fetchItem(dispatch, id, baseUrl))
return Promise.all(itemPromisesArray)
};
};
最后我们返回对promise.all的调用。
这意味着我们的thunk action creator会返回一个承诺,它会等待所有我们的子承诺,这些承诺代表要完成的每个提取(请求成功)或第一次拒绝(请求失败)
看到它返回一个接受调度的函数。这个返回的函数是将在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 action creator
因此,对我们的thunk动作创建者的测试将包括以下步骤:
但是,为了创建此测试,我们需要执行其他两个子步骤:
拦截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 开头的网址发出的任何HTTP请求 并以确定的方式回应状态代码和响应。
创建我们的Mock Redux商店
在测试文件中,从redux-mock-store库导入配置存储功能,以创建我们的虚假商店。
import configureStore from 'redux-mock-store';
此模拟存储将在数组中调度的操作将在您的测试中使用。
由于我们正在测试一个thunk动作创建器,我们的模拟商店需要在我们的测试中配置redux-thunk中间件
const middlewares = [ReduxThunk];
const mockStore = configureStore(middlewares);
Out mock store有一个store.getActions方法,在调用时会给我们一个包含所有以前调度过的动作的数组。
最后,我们调度thunk action creator,它返回一个promise,当所有单独的topicId fetch promise被解析时,它将被解析。
然后,我们进行测试断言,以比较调度到模拟商店的实际操作与我们预期的操作。
测试我们的thunk动作创建者在Mocha中返回的承诺
因此,在测试结束时,我们将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
};
};