开始测试React Native App(下篇)

2,609 阅读10分钟

前言:


开始测试React Native App(上篇)中编写了redux-upload-queue针对ReducerAction Creator的单元测试,测试代码可以在这里查阅。这篇文章基于开始测试React Native App(上篇)继续完成集成测试以及E2E测试。

集成测试

Action Creator的测试中,引入了redux-mock-store库,按官方的话来说,这个库只是用来测试Redux async action creatorsmiddleware,它不是用来测试reducer相关的逻辑,换句话来说它不会更新Redux Store,所以如果你想把reduceraction结合在一起测试建议使用redux-actions-assertions。 笔者在刚学测试时没有认真看文档这段话,导致写出了如下代码:

const rootReducer = combineReducers({
    upload: UploadReducer
})
let initState = {}
export const store = mockStore((actions) => {
    let currentState = initState
    actions.forEach(action => {
        currentState = rootReducer(currentState, action)
    });
    return currentState
})

变相的使用redux-mock-store实现了结合reduceraction的测试,能更改Redux Store,在性能上肯定是不优的,每次获取State都要遍历所有派发的actionreducer。所以还是建议使用redux-actions-assertions,在该篇文章中采用的是不优的解决方案。

在解决了以上测试的技术点后,就可以开始写组合reduceraction在一起的集成测试了:

import * as UploadActions from '../UploadActions'
import config, {store} from './UploadConfig'

afterEach(() => {
  store.clearActions()
  fetch.resetMocks()
})

...
//使用reducer和action模拟多张图片部分上传失败,重新上传成功的集成测试
it('upload mult fail and reupload action test', () => {
  fetch.mockResponses(
    [
      JSON.stringify({ error: null, id: '123456' })
    ],
    [
      JSON.stringify({ error: null, id: '123456' })
    ],
    [
      JSON.stringify({ error: new Error('fail') })
    ],
    [
      JSON.stringify({ error: null, id: '123456' })
    ],
  )

  store.dispatch(UploadActions.registerUpload({upload: 'uploadKey'}))
  store.dispatch(UploadActions.pushUploadItem({upload: 'uploadKey', name: 'fileOne', filePath: 'filePathOne'}))
  store.dispatch(UploadActions.pushUploadItem({upload: 'uploadKey', name: 'fileTwo', filePath: 'filePathTwo'}))
  store.dispatch(UploadActions.pushUploadItem({upload: 'uploadKey', name: 'fileThree', filePath: 'filePathThree'}))
  return store.dispatch(UploadActions.upload('uploadKey', config))
          .then(() => {
            return store.dispatch(UploadActions.upload('uploadKey', config))
          })
          .then(() => {
            expect(store.getActions()).toMatchSnapshot()
            expect(store.getState()).toMatchSnapshot()
          })
})

上面的测试代码首先派发出注册上传队列的动作(UploadActions.registerUpload),然后依次派发出在注册的上传队列中添加上传项的动作(UploadActions.pushUploadItem),再派发异步上传动作(UploadActions.upload)开始上传,因为使用fetch.mockResponsesmock了多次网络请求的返回结果来模拟上传的结果,所以模拟出了第一次上传时第三个文件(fileThree)上传失败,失败后再次派发上传动作UploadActions.upload返回成功,判断整个流程走完后store.getActions()store.getState()是否符合预期。

这个测试用例涉及到了派发action,使用reducer处理action,以及更改Store的状态,所以它是一个集成测试,也是单元测试的组合测试。

示例代码

其实集成测试更加的符合初学者对测试的直观想法,比如当我说我要测试上传组件的redux逻辑是否有问题时,自然而然就会想到要派发一系列action,再看reducer是否能正常的处理这些actionStore结果是否符合预期。在redux-upload-queue这个组件中,不但实现了Redux处理上传队列的整套逻辑,还使用HOC的方式,让任意组件可以快速的集成上传队列功能,例如:

...
import {redux_upload} from 'redux-upload-queue'

class Foo extends Component {
  componentDidMount() {
    this.props.pushUploadItem('fileOnePath', 'fileOne')
    this.props.startUpload()
  }
  render() {return <View/>}
}
...
export default redux_upload({ upload: 'uploadKey', config: config })(Foo)

上面的示例中对组件Foo快速的集成了上传队列的功能,那么redux_upload是否能正确的让被包裹的组件有上传功能呢?我们可以写以下集成测试用例来证明:

import config, {store, Foo} from './UploadConfig'
import uploadComponent from '../UploadComponent'
...
test('uploadComponent new', () => {
    fetch.mockResponseOnce(JSON.stringify({ error: null, id: '123456' }))
    
    const Component = uploadComponent({ upload: 'uploadKey', config: config })(Foo)
    const componentWrap = shallow(
        <Component store={store}/>
    )
    const fooWrap = componentWrap.shallow()
    const fooProps = fooWrap.props()
    fooProps.pushUploadItem('fileOnePath', 'fileOne')
    return fooProps.startUpload().then(() => {
        expect(store.getActions()).toMatchSnapshot()
    })
})

通过shallow来模拟组件装载,然后使用ShallowWrapperprops()来获取被装载的Foo组件的所有属性,调用属性的pushUploadItemstartUpload方法来触发上传操作,预期会触发与之前测试上传队列Redux逻辑差不多的Actions,都是先注册队列,添加上传项然后开始上传等,只是注册的唯一标识符、添加上传项的对象以及数量、上传的结果不同。

注意<Component store={store}/>这一行代码,store={store}是用来给Redux connect提供Store的,相当于使用react-redux中的Provider:<Provider store={store}><Component/></Provider>示例代码

E2E测试


E2E测试就是编译安装App到模拟器或真机,在App中模拟用户的行为进行测试,一般用来测试App的主要流程(不是全部流程),因为E2E测试受周边环境影响较大(网络等因素),因此测试结果不完全可靠。

Detox

Detox是一个移动App自动化E2E灰盒测试框架,是第一个支持React Native项目的E2E测试框架,可以结合Jest使用,安装与配置也是很快的。

Detox设计原则中我们可以了解到它是一个灰盒测试框架,这种测试框架是从App的内部操控测试(通过testId等方式查询到UI元素,然后执行TapActions来触发各种手势输入等(模拟用户),最后通过isVisibleMatcher来判断期望值),保证App的核心流程正确。它依赖Native端的灰盒测试框架:EarlGrey for iOSEspresso for Android,使用基于JSON的反射机制,让JavaScript直接调用Native测试框架的方法,在JavaScript端提供了一系列易于使用API,完全的抽象了Native引擎下发生的复杂调用逻辑,因此它写出来的测试可读性高。 测试脚本与被测试的App的通信原理:

image.png

依赖websockets让运行在nodejs的测试脚本和运行在设备上的App通信,实现了真实的双工通信,相比与其他类似REST的协议要更快更灵活,运行在nodejs端的测试让它能在多个平台上运行。

测试与App同步

这种E2E测试脚本执行App流程,让人最困惑的的就是它是如何将测试与App同步,App复杂的操作(例如访问服务器数据或执行动画)经常需要大量的时间去完成,在这些操作完成之前我们不能继续执行测试代码,否则会使测试失败(例如正在测试登录流程,在没登录成功时你就断言进入首页,测试就会失败),那我们如何将测试与App同步?

经常会想到的解决方案是手动执行sleep()来同步,但是在不同的设备上,不同的网络状态下,执行相同的操作所花费的时间不同,手动sleep()要不会造成不必要的时间浪费,让测试变慢,要不时间不给充足会让测试直接失败。

Detox的同步方式是:自动同步,这种同步方式就像魔术一样,你在写了一行测试代码后写下一行测试代码无需关心中间的时间间隔问题(数据是不是还没有获取到,转场动画是否还没执行完等),Detox会等待App稳定之后才会去执行下一行代码。例如有一个已经发出的网络请求,那么直到网络请求完成测试才会执行下一行代码。

这种自动同步要百分之百的正确是非常困难的,经常会有一些异常情况,Detox正在对这些异常情况进行优化,因此大部分情况下都需要考虑同步问题。那如果遇到了同步问题应该怎么办呢?这里给出了具体的解决方案,包括:

  • 不能自动同步的原因。
  • 可以自动同步的场景。
  • 手动切换到非自动同步模式,然后使用waitFor做手动同步。
  • 使用react-native-repackager重写e2e下执行的代码,就像*.ios.js*.android.js一样,可以通过*.e2e.js来加载在E2E环境测试时运行的代码。

注意: 1、同步状态难以解救时要去看下一自己的代码是不是使用setTimeout等不当操作造成了滥用资源,内存泄漏。 2、react-native-repackager在0.55.*之后的版本因为这个PR而不再需要,但依然是通过定义E2E的flavor来使用特定的*.e2e.js自定义扩展名。

Mock

之前提到过,E2E测试受环境因素影响大,例如网络状态,模拟器中没有图片库,没有联系人等,想象一下如果我们能够在运行E2E测试时达到以下需求:

  • 使用本地的Mock HTTP Server来代替生产环境中真实的服务器访问(这里推荐我的美女同事写的两篇文章:前后端分离——数据mockjson-server 接入项目说明用来Mock服务器数据)。
  • 当运行在模拟器时,不去访问设备上的联系人,而是返回Mock的联系人。

在对真实项目E2E时,诸如以上的场景还有许多,因此可以使用build flavouring来自定义file extensions,然后编写Mock。这样可以大量减少受E2E运行结果受环境因素的影响,具体可以看react-native-repackager Better support for custom file extensions (and build flavours)

Artifacts

最后一个我特别喜欢的功能点,那就是Artifacts了,它可以在测试过程中以多种方式记录下测试过程,例如录像、截图、log等。大家有兴趣可以看文档,简单实用。

总结选择Detox进行E2E测试的原因:

  • 代码跨平台,因为它是在nodejs中执行的。
  • 可以在真机、模拟器中运行App。
  • 结合Jest使用时,易配置,上手难度小。
  • 使用async/await自动同步测试和App的状态,大部分情况下无需写waitFor
  • 使用Artifacts可以在测试过程中录制视频和截图。
  • 提供全面的Actions,例如点击(单点,多点,长按)、滑动(上下左右四个方向以及速度位置的控制)、输入文本、滚动等。
  • 提供Matcher获取UI元素,可以通过testID、文本内容、nativeViewType来定位UI,即可以查找到js端定义的UI,可以查找到native端定义的UI。
  • 提供Expect来断言期望值,可以断言UI元素是否存在,是否可见。

还有更多好用的API可以在文档中查阅。

可运行官方示例体验:github.com/wix/detox/t…

完(初)


之所有写【完(初)】,是因为在这里测试的初级学习就结束了,其实在学习测试的过程中Mock是一个很重要的概念,几乎无处不在Mock,想必大家在文章中也看到了我用的Mock库:

这些Mock不限于Jest中的Mock,还有在编写开发代码中的Mock,除了使用这些Mock库,在实际项目中写测试时也需要自己写大量的对第三方库或者对自己写的模块的Mock,以及React Native虽然自带Mock但是还有一些模块(Platform等)没有被Mock到,也要自己Mock。除了模块的Mock还有Function的Mock,数据的Mock,Global的Mock等等,具体参考Testing React Native Apps

Test Runner:

  • Jest:JavaScript测试框架

Test Utility:

欢迎关注我的简书主页:www.jianshu.com/u/b92ab7b3a… 文章同步更新^_^