簡體   English   中英

如何測試僅調度其他操作的Redux操作創建者

[英]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動作創建者的測試將包括以下步驟:

  1. 創建一個模擬商店。
  2. 派遣的thunk行動的創建者3.確保畢竟異步獲取完整的針對在一個陣列形實轉換動作創造者FETCH_PENDING動作已分發傳遞的每個主題ID。

但是,為了創建此測試,我們需要執行另外兩個子步驟:

  1. 我們需要模擬HTTP響應,因此我們不會向實時服務器發出實際請求
  2. 我們還想創建一個模擬商店,允許我們查看已分派的所有歷史操作。

攔截HTTP請求

我們想測試一次調用fetchAllItems動作創建者調度某個動作的正確數量。

好的,現在在測試中我們不想實際向給定的api發出請求。 請記住,我們的單元測試必須快速且確定。 對於我們的thunk動作創建者的給定參數集,我們的測試必須始終失敗或通過。 如果我們實際從我們測試中的服務器獲取數據,那么它可能會傳遞一次然后在服務器出現故障時失敗。

從服務器模擬響應的兩種可能方法

  1. 模擬Axios.get函數,以便它返回一個promise,我們可以使用我們預定義的錯誤強制解析我們想要或拒絕的數據。

  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!');

});

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.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM