前言
為了確保軟體質量,保證函式邏輯的正確性,我們一般會進行單元測試。本文主要講述了在基於dva框架的應用中,我們是如何對model中的reducer
和effect
進行單元測試的,以及背後的一點原理。
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開啟immer
為true
,這樣就可以保持原始的state的不變性。
[
'umi-plugin-react',
{
dva: {
immer: true
},
}
]
複製程式碼
這就是為什麼我們可以直接操作傳入的state,改變其值,因為此時傳入的state並不是最原始的state,而是一個Proxy
物件,當我們改變它的值,immer會自動和初始state做merge
的操作。如下圖所示:
測試程式碼
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
的結果來實行不同的操作。當返回結果的success
為true
,即沒有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' }
})
複製程式碼
這個時候函式已經執行完獲取response
中id
的操作並且進行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的測試方法。當然,大家在平時寫程式的過程中,也要考慮到如何讓測試更方便更簡潔合理,而不是隻為了實現功能而寫程式碼。