dva應用中reducers和effects的單元測試實戰

Lingzhi發表於2019-03-24

前言

為了確保軟體質量,保證函式邏輯的正確性,我們一般會進行單元測試。本文主要講述了在基於dva框架的應用中,我們是如何對model中的reducereffect進行單元測試的,以及背後的一點原理。

dva框架

這是一套由國內開發團隊開發的輕量級資料流框架,整合了當前流行JavaScript庫比如redux,react-router和redux-saga,易學易用,使開發中的資料流管理變得更加簡單高效。

測試reducers

由於reducer都是純函式,因此,只要給定一個payload,就會有確定的輸出,不會因函式外部其他環境影響而改變輸出結果。

reducer函式樣例

例如,我有個reducer函式saveNotify,其實現了將payload中的notification資訊更新到state中:

saveNotify(state, { payload }) {
    let {notification} = state
    if (!payload) {
      return state
    }
    notification = {...notification, ...payload.notification}
    notification.visible = true
    state.notification = notification
}
複製程式碼

值得注意的是,我們可以在.umirc.js檔案中配置umi-plugin-react的屬性,設定dva開啟immertrue,這樣就可以保持原始的state的不變性。

[
    'umi-plugin-react',
      {
        dva: {
          immer: true
        },
      }
]
複製程式碼

這就是為什麼我們可以直接操作傳入的state,改變其值,因為此時傳入的state並不是最原始的state,而是一個Proxy物件,當我們改變它的值,immer會自動和初始state做merge的操作。如下圖所示:

dva應用中reducers和effects的單元測試實戰

測試程式碼

const payload = {title:'Create Role Successfully.', status:'successful'}
const initState = {notification: {}}

describe('Notification Models:', () => {
   it('should save Notify payload:', () => {
      const saveNotify = AppModule.reducers.saveNotify
      const state = initState
      const result = saveNotify(state, { payload : {notification: payload} })
      expect(state.notification.status).toEqual('successful')
      expect(state.notification.visible).toEqual(true)
  })
}}
複製程式碼

測試effects

effects雖然不是純函式,會涉及諸如API服務呼叫,讀取檔案和資料庫的操作等,但由於在單元測試中,也就是說在這麼一個函式中,我們並不需要去關心其呼叫API的過程,只要關心我們的函式是否有發起API請求即可。在後續邏輯中,需要用到調API返回的結果,那麼我們可以直接給它模擬一個結果傳入。

effect函式樣例

例如,我有這麼一個effect函式,其作用是發起createInfo的API請求,然後根據reponse的結果來實行不同的操作。當返回結果的successtrue,即沒有error時,進行頁面跳轉並且執行put操作改變state中的notification狀態,彈出notification訊息框。當然,我這裡省略了出現error的情況處理。

*createInfo({ payload: { values } }, { call, put }) {
      const { data, success } = yield call(Service.createInfo, values)
      if (data && success) {
        const { id } = data
        router.push(`/users/${id}/view`);

        const notification = {title:'Create information Successfully.', status:'successful'}
        yield put({ type: 'app/notify', payload:{notification}})
      }
}
複製程式碼

測試過程和原理

effect函式其實是一個generator函式,很多人以為寫effect測試只需呼叫.next()即可,但卻未深究為什麼要這麼做。

在ES6中新添了generator函式,generator函式和普通函式的差別即為:它是可中途停止執行的函式。它是一個解決非同步請求的很好的方案。每遇到yield關鍵字,它就會自動暫停,直到我們手動去讓它繼續開始。dav封裝了redux-saga,那麼redux-saga的Effects管理機制會自行來啟動開始讓函式繼續執行。而在測試中我們則需要呼叫.next()手動啟動繼續執行。

初始化:首先我們需要初始化generator函式,此時並沒有開啟執行,所以這一步在createInfo這個effect函式中什麼也沒有發生。

const actionCreator = {
    type: 'info/createInfo',
    payload: {
        values: {
            name: 'abc',
            description: 'xxx',
        }
    }
}
const generator = info.effects.createInfo(actionCreator, { call, put })
複製程式碼

開始執行: 我們呼叫generator.next()會啟動函式的執行,函式會在遇到yield關鍵字時停止,這時候還沒有去發起呼叫API服務,只是準備去發起。呼叫.next()會返回一個物件:

{ value: xxxx, done: false}
複製程式碼

value表示的是yield接下來該做的事,即call API這個行為。

let next = generator.next()
expect(next.value).toEqual(call(Service.createInfo, actionCreator.payload.values))
複製程式碼

繼續執行:我們再接著呼叫.next()啟動執行,在這一步函式會真正地去執行call(Service.createInfo, actionCreator.payload.values)。 拿到結果後,進入到if語句,直到遇到下一個yield關鍵字而暫停。 由於執行call會返回一個response執行結果,在單元測試中我們就需要在呼叫.next()時傳入一個模擬的response

next = generator.next({
        success: true,
        data: { id: '123456' }
})
複製程式碼

這個時候函式已經執行完獲取responseid的操作並且進行router跳轉,且又在遇到下一個yield關鍵字時暫停。這時候我們可以斷言mock的router.push有沒有執行,並且判斷當前next的value是否為put操作:

router.push = jest.fn()
expect(router.push).toHaveBeenCalledWith(`/list/123456/view`)
const notification = {title:'Create Information Successfully.', status:'successful'}
expect(next.value).toEqual(put({ type: 'app/notify', payload:{notification}}))
複製程式碼

當我們再次呼叫.next()讓其繼續執行的時候,接下來的操作已經沒有yield關鍵詞了,因此函式會一直執行直到結束,而此時的value也會是undefined

next=generator.next()
expect(next.value).toBeUndefined()
複製程式碼

最後的話

希望大家能通過我的小例子不僅能初步學習dva框架的model中reducer和effect函式的測試流程,也能理解effect函式的執行過程以及saga的測試方法。當然,大家在平時寫程式的過程中,也要考慮到如何讓測試更方便更簡潔合理,而不是隻為了實現功能而寫程式碼。

相關文章