[英]How do I test `image.onload` using jest in the context of redux actions (or other callbacks assigned in the action)
[英]How do I test a Redux action creator that only dispatches other actions
我在測試一個動作創建器時遇到了麻煩,該動作創建器只是遍歷傳遞給它的數組,並為該數組中的每個項目調度一個動作。 這很簡單,我似乎無法弄明白。 這是動作創建者:
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
。
編寫和測試Redux Thunk Action Creators的指南,它為API提供基於Promise的請求
前言
此示例使用Axios ,它是一個基於promise的庫,用於發出HTTP請求。 但是,您可以使用不同的基於承諾的請求庫(如Fetch)來運行此示例。 或者只是在promise中包含一個普通的http請求。
Mocha和Chai將在此示例中用於測試。
使用Redux操作表示請求的有狀態
來自redux文檔:
當您調用異步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動作創建器 ,它將使用Redux-thunk中間件來允許我們在將來發送額外的異步動作。
Thunk Action的創建者如何運作?
我們的thunk action creator會調度與多個 topicId的提取相關的操作。
這個單一的thunk動作創建者是一個動作創建者,它將由我們的redux thunk中間件處理,因為它適合與thunk動作創建者相關聯的簽名,即它返回一個函數。
當調用store.dispatch時,我們的操作將在到達商店之前通過中間件鏈。 Redux Thunk是一個中間件,它會看到我們的動作是一個函數,然后讓這個函數訪問商店調度和獲取狀態。
這是Redux thunk里面的代碼:
if (typeof action === 'function') {
return action(dispatch, getState, extraArgument);
}
好的,這就是為什么我們的thunk action creator返回一個函數。 因為這個函數將被中間件調用並讓我們訪問調度和獲取狀態,這意味着我們可以在以后調度進一步的操作。
寫我們的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 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動作創建者
因此,對我們的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開始對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動作創建器,該動作創建器返回一個promise,該promise在解析所有單個topicId fetch promise時解析。
然后,我們進行測試斷言,以比較調度到模擬存儲的實際操作與我們預期的操作。
測試我們的摩卡動作創建者返回的承諾
因此,在測試結束時,我們將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
};
};
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.