React Native单元测试理论与实践

自动化测试、单元测试、集成测试、E2E

自动化测试是为了代替人工,实现一些重复工作,提高工作效率,其一般遵循测试金字塔原则,如下图:

test-pyramid.png

即推荐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 文件。

一些注意点

  1. 对于需要mock export 出来的function, 且function不是在某个类里面,直接mock会提示read-only无法修改,可以通过import as来导入,然后通过别名.方式mock
    Error: "requestIntegralPackageList" is read-only.
  1. 测试某个组件,如果组件内部有网络请求等异步方法,需要在测试块前加上async,否则会报You are trying toimporta file after the Jest environment has been torn down等错误, 也可以通过Timer mocks方式,在test块前,加上jest.useFakeTimers(),想要获取网络请求之后的组件层级,以及state,可以使用jest.runAllTimers()

参考文献

如何自动化测试 React Native 项目 (下篇) - 单元测试

如何自动化测试 React Native 项目 (上篇) - 核心思想与E2E自动化

开始测试React Native App(下篇)

开始测试React Native App(上篇)

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容