前言
为了确保软件质量,保证函数逻辑的正确性,我们一般会进行单元测试。本文主要讲述了在基于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的测试方法。当然,大家在平时写程序的过程中,也要考虑到如何让测试更方便更简洁合理,而不是只为了实现功能而写代码。