自动化测试、单元测试、集成测试、E2E
自动化测试是为了代替人工,实现一些重复工作,提高工作效率,其一般遵循测试金字塔原则,如下图:
即推荐70%的单元测试,20%的集成测试,10%的E2E测试。占比最大的是单元测试,也是运行最快,花费最小,效果最高的一环,其次是集成测试,最上层是E2E(端到端)测试,E2E测试是最真实模拟用户的一层测试,也是最依赖build构建与测试环境的一层,也是运行最慢,花费最大的一层。
单元测试VS.集成测试
单元测试一般是针对某个function,某个class,或者某个component,其针对的是最小独立单元。集成测试是保证每个独立单元组合在一起时,也能够正常运行。两者配合的原则是,尽量避免重复测试,即单元测试覆盖的部分,不要再用集成测试实现一遍。
实践
本文的RN单元测试实践,是基于 jest + react-test-renderer环境
Snapshot
对于React-native, UI的更新渲染是交给React去实现的,开发者只需要关心和处理不同情况下的props和state即可。当props和state确定时,Snapshot能保证当前生成的组件序列化树是不变的,否则jest会通过git-diff形式提示开发者差异点。
example:
function AnimLinearGradientBtn({style, onClick, startColor, endColor, text, androidShowElevation}) {
return (
<Animated.View style={{
shadowColor:"#1A84E7",
shadowOffset: {
width: 0,
height: 3
},
shadowRadius: Dimens.dx_13,
shadowOpacity: 0.46,
elevation: 6,
...style,
}}>
<LinearGradient
start={{
x: 0,
y: 0
}} end={{
x: 1,
y: 0
}}
colors={[startColor, endColor]}
style={{...styles.button, elevation: androidShowElevation? 6: 0}}>
<TouchableOpacity style={styles.buttonInner} onPress={() => {onClick()}}>
<Text
allowFontScaling={false}
numberOfLines={1}
ellipsizeMode={'tail'}
style={styles.buttonText}>{text}</Text>
</TouchableOpacity>
</LinearGradient>
</Animated.View>
)
}
import React, {TouchableOpacity} from 'react-native'
import TestRender from 'react-test-renderer'
import AnimLinearGradientBtn from "../AnimLinearGradientBtn";
it('AnimLinearGradientBtn snapshot ', function () {
let render = TestRender.create(AnimLinearGradientBtn(props))
expect(render.toJSON()).toMatchSnapshot()
});
测试组件交互
交互测试一般测试某个方法是否被调用,调用的时候传参是否正确,以及组件状态更改是否符合预期
let onClickMock,props
beforeEach(() => {
onClickMock = jest.fn()
props = {
style: {},
onClick: onClickMock,
text: 'hello world'
}
})
it('should AnimLinearGradientBtn onClick response', function () {
let render = TestRender.create(AnimLinearGradientBtn(props))
render.root.findByType(TouchableOpacity).props.onPress()
expect(onClickMock).toBeCalledTimes(1)
});
功能函数测试
好的React native代码模式,应该是View和数据处理分开来,数据处理应该分散到Reducer、Action Handler、Selectors、Utils这几个模块中。优点是模块化编程,代码逻辑更清晰,也更方便编写单元测试。
Reducer
reducer类似于普通函数,测试特定输入能返回期待的输出即可,用snapshot能减少对多个字段的单独判断
export default function headRedux(state = 'UNUSED', action){
if (action.type === 'S_FILTER') {
return action.payload.filter
} else {
return state
}
}
import headRedux from '../headRedux'
describe('headRedux', function () {
it('action.type === S_FILTER', function () {
expect(headRedux('UNUSED',{'type' : 'S_FILTER', 'payload' : {'filter' : 'dada'}})).toMatchSnapshot()
});
it('action.type !== S_FILTER', function () {
expect(headRedux('UNUSED',{'type' : 'S_FILTER11', 'payload' : {'filter' : 'dada'}})).toMatchSnapshot()
});
});
Action Handler
非异步action的测试方法与纯函数测试一样。异步action测试则需要多几个步骤,首先mock dispatch, 然后将被mock的dispatch作为参数传给异步action,最后通过断言判断dispatch参数是否符合预期
export function fetchIntegralProducts(pageIndex,pageSize,states) {
return dispatch => {
....
dispatch(fetchDataRequest());
requestIntegralPackageList(pageIndex,pageSize,states).then(result =>{
if (pageIndex === 1){
dispatch(fetchDataSuccess(result.data))
}else {
dispatch(fetchDataNextSuccess(result.data))
}
....
}).catch(error => {
....
dispatch(fetchDataError(error))
})
}
}
let requestIntegralPackageListMock
beforeAll(() => {
jest.useFakeTimers();
})
beforeEach(() => {
jest.clearAllTimers()
requestIntegralPackageListMock = jest.fn(() => {
return new Promise(resolve => {
resolve({data: []})
})
})
Ticket.requestIntegralPackageList = requestIntegralPackageListMock
})
afterAll(() => {
jest.useRealTimers();
})
describe('integralAction', function () {
it('fetchIntegralProducts pageIndex 1 success', function () {
const dispatch = jest.fn()
IntegralAction.fetchIntegralProducts(1,1,{})(dispatch)
expect(dispatch.mock.calls[0][0]).toMatchSnapshot()
jest.runAllTimers();
expect(dispatch.mock.calls[1][0]).toMatchSnapshot()
});
});
Selectors、Utils
这两个跟纯函数类似,就不再赘述了
集成测试
见 RobotMsgTabPages 文件。
一些注意点
- 对于需要mock export 出来的function, 且function不是在某个类里面,直接mock会提示read-only无法修改,可以通过import as来导入,然后通过别名.方式mock
Error: "requestIntegralPackageList" is read-only.
- 测试某个组件,如果组件内部有网络请求等异步方法,需要在测试块前加上async,否则会报
You are trying to
importa file after the Jest environment has been torn down
等错误, 也可以通过Timer mocks方式,在test块前,加上jest.useFakeTimers(),想要获取网络请求之后的组件层级,以及state,可以使用jest.runAllTimers()
参考文献
如何自动化测试 React Native 项目 (下篇) - 单元测试