2017-01-07 18 views
0

我在测试动作创建器时遇到问题,该动作创建器只是循环传递给它的数组,并为该数组中的每个项目分派动作。这很简单,我似乎无法弄清楚。这里是行动的创建者:如何测试只发送其他动作的Redux动作创建器

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

你收到这个错误,因为你没有在函数返回任何东西由'fetchAllItems'返回。 '.forEach'也不会返回任何东西。 就测试而言,您可能必须使用Rewire或类似的东西来模拟'fetchItems'(我对此有点生疏,不好意思)。 – DonovanM

+0

@DonovanM是正确的,你没有返回任何东西。你也可以将'topicIds'映射到一个promise数组,然后使用'Promise.all()'来解决。 –

+0

@ OB3是否可以模拟'dispatch'和'fetchItem'并将这些模拟版本(可能是间谍)传递给'fetchItems'?也许这样:'fetchAllItems([1,2])(mockDispatch,mockFetchItems)'?谢谢。 –

回答

5

指南编写和测试终极版咚行动创造者,使一个基于无极请求的API

序言

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

摩卡和柴将在本例中用于测试。

表示请求与终极版动作

从终极版文档中有状态:

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的外部资源。

3种其表示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 } 
} 

请注意,您应该减速妥善处理这些动作。

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

Axios公司是基于许请求库。所以axios.get方法向指定URL的请求,并返回如果成功,否则这一承诺将被拒绝

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

如果我们的爱可信请求成功我们的承诺将得到解决,将解决一个承诺代码,然后执行。这将派遣FETCH_FULFILLED行动为我们与我们的请求的响应给定的主题ID(我们的话题数据)

如果爱可信请求不成功我们代码.catch将被执行并分派FETCH_REJECTED行动其中将包含主题ID和请求期间发生的错误。

现在我们需要创建一个动作创建器来启动多个topicIds的获取过程。

因为这是一个异步的过程中,我们可以使用一个thunk行动的创建者将使用终极版,形实转换的中间件,让我们派遣在未来更多的异步操作。

Thunk Action创建者如何工作?

我们的thunk动作创建者分派与提取多个 topicIds相关联的操作。

这个单一的thunk动作创建者是一个动作创建者,它将被我们的redux thunk中间件处理,因为它适合与thunk动作创建者相关的签名,也就是它返回一个函数。

当store.dispatch被调用时,我们的操作将在它们到达商店之前通过中间件链。 Redux Thunk是一款中间件,会看到我们的动作是一个功能,然后给这个函数访问存储分派和获取状态。

这里是终极版的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正如我们在前面的功能终极版-的thunk看到调用由我们的调度和行动的getState返回创造者的功能。

我们可以像这样

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

记住终极版 - thunk是不是唯一的解决将此定义为我们的thunk行动的创建者返回的函数内的精氨酸。如果我们想派发promise而不是函数,我们可以使用redux-promise。不过,我会建议从最简单的解决方案开始使用redux-thunk。

测试我们的thunk行动的创建者

。因此测试我们的thunk行动的创建者将包括以下步骤:

  1. 创建一个模拟商店。
  2. 调度thunk动作创建者 3.确保所有异步提取已完成,以便将数组传递给thunk动作创建者的每个主题ID FETCH_PENDING操作已分派。

然而,我们需要做的其他两个子步骤中,我们需要为了创造这个测试来进行:

  1. 我们需要模拟HTTP响应,所以我们不要对现场实时请求服务器
  2. 我们还希望创建一个模拟存储,使我们能够查看已分派的所有历史操作。

拦截HTTP请求

我们想测试某个动作的正确数量是由到fetchAllItems行动的创建者单个呼叫调度。

好了,现在在测试中我们不想实际提出给定的api请求。记住我们的单元测试必须是快速和确定性的。对于我们的thunk动作创建者的一组给定的参数,我们的测试必须总是失败或通过。如果我们实际上从我们的测试中的服务器获取数据,那么它可能会传递一次,然后在服务器关闭时失败。从嘲讽服务器的响应的

两种可能的方式

  1. 嘲笑Axios.get功能,使得它返回一个承诺,我们可以迫使我们想要或拒绝与我们预定义的错误数据来解决。

  2. 使用像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!'); 

}); 

诺克截取开始http://www.example.com 到一个URL进行的任何HTTP请求,并与状态码和应答的确定的方式进行响应。

从终极版 - 模拟店库创建我们的模拟终极版店

在测试文件导入配置存储功能来创建我们的假店。

import configureStore from 'redux-mock-store'; 

这个模拟商店将在您的测试中使用数组中的调度动作。

由于我们正在测试一个thunk行动的创建者我们的模拟店必须在我们的测试

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

,进行模拟店与终极版,形实转换中间件配置有store.getActions方法,该方法调用的时候给了我们一个所有先前分派的动作的数组。

最后我们派遣thunk动作创建器,它返回一个promise,当所有的个体topicId获取promise都被解析时,这个promise会解析。

然后我们做出我们的测试断言,比较分派给模拟商店的实际行为与我们预期的行为。

用于测试由我们的thunk行动的创建者在摩卡

返回的承诺,因此,在测试结束时,我们派遣我们的thunk行动的创建者,以模拟商店。我们不能忘记返回这个调度调用,以便当thunk动作创建者返回的promise被解析时,断言将在.then块中运行。

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

请参阅下面的最终测试文件:

最终测试文件

测试/索引。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 
    }; 
}; 
相关问题