2017-01-07 52 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 
    }; 
}; 
相關問題