开始测试React Native App(上篇)

栏目: IOS · Android · 发布时间: 6年前

内容简介:我是测试小白,小小白,小小小白,最近想在成了一定规模的项目中引入测试,于是找了许些资料学习,现在已经在项目中成功引入。于是想在思路明朗和记忆深刻的时候总结下学习路径以及写测试中遇到的难点、坑点、注意点。给自己的近段学习成果做个总结,同时也希望能帮助到和我一样初入测试的人。React Native在0.56、0.57版本上测试运行有各种各样的问题,例如:扩展阅读:

前言

我是测试小白,小小白,小小小白,最近想在成了一定规模的项目中引入测试,于是找了许些资料学习,现在已经在项目中成功引入。于是想在思路明朗和记忆深刻的时候总结下学习路径以及写测试中遇到的难点、坑点、注意点。给自己的近段学习成果做个总结,同时也希望能帮助到和我一样初入测试的人。

注意注意特别注意!!!

React Native在0.56、0.57版本上测试运行有各种各样的问题,例如: Can't run jest tests with 0.56.00.56 regression: jest.mock only works when defined in jestSetup.js, not in individual Snapshots tests 以及笔者还没遇到的问题,笔者亲测:"Can't run jest tests with 0.56.0"这个问题在0.57中已经解决,“0.56 regression: jest.mock only works when defined in jestSetup.js, not in individual Snapshots tests”这个问题在0.57中依然存在。所以文章示例建议在0.55.4版本中运行。

初入测试一定要明白的重要概念

  • 自动化测试
  • 测试金字塔
  • 单元/集成/e2e测试

扩展阅读: 如何自动化测试 React Native 项目 (上篇) - 核心思想与E2E自动化 了解以上概念。

随着项目越来越大,新增需求对于开发而言或许不算太大工作量,但是对于测试而言,特别是回归测试,压力会徒然增加很多,如果是手工测试或者是放弃一些测试用例,都是不稳定的测试。所以自动化测试的重要性就体现出来了,自动化测试的大体思路即是”测试金字塔“,测试金字塔从上到下分别是E2E测试、集成测试、单元测试。E2E测试是需要真实编译打包在模拟器上或者真机上模拟用户行为走测试流程,测试结果受网络,弹窗,电话等不可控影响较大,因此不能过于信任,因此E2E测试出的Bug最好能到集成测试中重现,集成测试中的出现的Bug最好能在单元测试中重现,若不能重现则应该加入更多的单元/集成测试来重现Bug。集成和单元测试都不需要编译打包运行,因此它们的执行速度非常快,所以项目中测试代码量应该是单元测试大于集成测试,集成测试大于E2E测试,从而形成自动化测试金字塔。

  • Snapshot
  • Mock
  • JavaScript Testing utility :例如Detox、Enzyme
  • JavaScript Test runners and assertion libraries :例如Jest

文章后面会重点解释以上概念。

React Native对于测试的支持

If you're interested in testing a React Native app, check out the React Native Tutorial on the Jest website.

Starting from react-native version 0.38, a Jest setup is included by default when running react-native init .

通过 React NativeJest 官方描述,可以得到结论:在react-native 0.38及后续版本在 react-native init 时已经默认植入了Jest测试库,所以我们可以0配置开始尝试编写测试代码。

使用以下方式开始尝试一下吧 (*^^*) 创建 iosandroid 同级目录下创建 __test__ 文件夹,在 __test__ 文件夹下创建 helloworld.test.js 文件,并输入以下代码:

it('test',()=>{
  expect(42).toEqual(42)
})
复制代码

在终端执行: npm test 查看测试结果。 入门是不是超简单o(* ̄ ̄*)o!

注:不是一定要在 iosandroid 同级的目录创建 __test__ 文件夹才能写测试代码,项目下的 *.test.js 都可以执行测试。

Jest必备知识

请阅读 jestjs.io/docs/en/get… 的 Introduction 章节的前5篇文章(到Mock Function为止),Guides章节的第一篇文章。

Jest 是一个运行测试和断言的库(Test Runner and assertion libraries),Jest通过Expect来断言当前结果和预期结果是否相同,这些结果是这里所涉及到的数据类型。Jest使用Mock来模拟一些Function、Module以及Class来方便测试(Mock测试中不需要真实去执行的代码,例如Fetch,Platform.OS等)。

Snapshot翻译成中文是快照的意思,以前的UI测试是执行测试脚本并在停留的页面上截图,当再次执行相同的测试脚本时会拿前后的截图做对比,如果像素相同则测试通过,像素不相同则测试不通过。在Jest中对React的UI测试可以通过Snapshot生成序列化结构树(文本形式),对比前后生成的结构树即可。Snapshot不仅仅可以用来测试UI,它可以用来测试任何可以序列化的结构,例如Action、Store等,在文章后面会有所提及。

前期技术储备好了我们就可以开始着手写测试了^_^

单元测试

Redux 逻辑测试

官方推荐阅读: Testing React Native with the new Jest — Part II

Redux中的Reducer测试

Reducer是纯函数,也就是说在有相同的输入值时,就一定是相同的输出,因此是很容易测试的。

it('start upload action will combine upload\'s watting queue and failed queue then update upload\'s uploading state', () => {
    let currentState = Map({
        'uploadTestKey': new Upload({
            name: 'uploadTestKey',
            wattingQueue: List([
                new UploadItem({
                    name: 'fileTwo',
                    filepath: 'fileTwoPath'
                })
            ]),
            uploadedQueue: List([
                new UploadItem({
                    name: 'fileThree',
                    filepath: 'fileThreePath'
                }),
            ]),
            failedQueue: List([
                new UploadItem({
                    name: 'fileOne',
                    filepath: 'fileOnePath'
                }),
            ]),
        })
    })
    currentState = UploadReducer(currentState, UPloadActions.startUpload({upload: 'uploadTestKey'}))
    expect(currentState).toMatchSnapshot()
})
复制代码

上面的代码示例是测试 UploadReducer 对固定输入 currentStateUPloadActions.startUpload({upload: 'uploadTestKey'}) 的输出是否正确,这里需注意以下两点:

1、要确保第一次运行 npm run test 后产生的 __snapshots__/<测试文件名称>.snap 里面内容的正确性。因为 expect(currentState).toMatchSnapshot()expect(value).toEqual(someValue) 的写法不同,后一种可以在写测试用例时直接给出期望值,前一种是测试用例运行完自动将期望值写入到了 __snapshots__/<测试文件名称>.snap 文件中,因此在第一次运行完测试用例我们需要确认生成的 snapshot 的正确性。 toMatchSnapshot() 的好处是不需要copy代码在测试用例中,如果不使用 toMatchSnapshot() ,我们的测试用例将写成以下形式:

it('start upload action will combine upload\'s watting queue and failed queue then update upload\'s uploading state', () => {
    let currentState = Map({
        'uploadTestKey': new Upload({
            name: 'uploadTestKey',
            wattingQueue: List([
                new UploadItem({
                    name: 'fileTwo',
                    filepath: 'fileTwoPath'
                })
            ]),
            uploadedQueue: List([
                new UploadItem({
                    name: 'fileThree',
                    filepath: 'fileThreePath'
                }),
            ]),
            failedQueue: List([
                new UploadItem({
                    name: 'fileOne',
                    filepath: 'fileOnePath'
                }),
            ]),
        })
    })
    currentState = UploadReducer(currentState, UPloadActions.startUpload({upload: 'uploadTestKey'}))
    expect(currentState.is(
        Map({
        'uploadTestKey': new Upload({
            name: 'uploadTestKey',
            wattingQueue: List([
                new UploadItem({
                    name: 'fileTwo',
                    filepath: 'fileTwoPath'
                }),
                new UploadItem({
                    name: 'fileOne',
                    filepath: 'fileOnePath'
                }),
            ]),
            uploadedQueue: List([
                new UploadItem({
                    name: 'fileThree',
                    filepath: 'fileThreePath'
                }),
            ]),
            failedQueue: List([]),
        })
    })
    )).toBe(true)
})
复制代码

这样就造成了代码冗余,这时 snapshot 的重要性就提现出来了。

2、既然是单元测试,那我们写的每个测试用例的职责都要单一,不要在单元测试中写出集成测试出来,这是刚学测试经常难以区分的。测试的语法并不难,难得是写出什么样的测试用例。例如以上的测试用例是测试一个上传队列组件,它的 reducer 可以处理多个 action ,例如 pushdeleteupload 等,那我们应该怎样为这个 reducer 写单元测试呢?笔者一开始就跑偏了,写出了这样的测试用例,各位看官可以看看:

describe("upload component reducer test", () => {
    describe("one file upload", () => {
        let currentState = Map({})
        beforeAll(() => {
            currentState = UploadReducer(currentState, UPloadActions.registerUpload({upload: 'uploadTestKey'}))
            expect(currentState).toMatchSnapshot()
        })
    
        afterAll(() => {
            currentState = UploadReducer(currentState, UPloadActions.destroyUpload({upload: 'uploadTestKey'}))
            expect(currentState).toMatchSnapshot()
        })
        ...
        test("handle upload success", () => {
            let state = UploadReducer(currentState, UPloadActions.pushUploadItem({upload: 'uploadTestKey', name: 'fileOne', filePath: 'fileOnePath'}))
            expect(state).toMatchSnapshot()
            state = UploadReducer(state, UPloadActions.startUpload({upload: 'uploadTestKey'}))
            expect(state).toMatchSnapshot()
            state = UploadReducer(state, UPloadActions.startuploadItem({upload: 'uploadTestKey'}))
            expect(state).toMatchSnapshot()
            state = UploadReducer(state, UPloadActions.uploadItemSuccess({upload: 'uploadTestKey', id: '12345'}))
            expect(state).toMatchSnapshot()
            state = UploadReducer(state, UPloadActions.uploadComplete({upload: 'uploadTestKey'}))
            expect(state).toMatchSnapshot()
        })

        test("handler upload failed", () => {
          ...
        })

        test("handler reupload success", () => {
            let state = UploadReducer(currentState, UPloadActions.pushUploadItem({upload: 'uploadTestKey', name: 'fileOne', filePath: 'fileOnePath'}))
            state = UploadReducer(state, UPloadActions.startUpload({upload: 'uploadTestKey'}))
            state = UploadReducer(state, UPloadActions.startuploadItem({upload: 'uploadTestKey'}))
            state = UploadReducer(state, UPloadActions.uploadItemFailed({upload: 'uploadTestKey'}))
            state = UploadReducer(state, UPloadActions.uploadComplete({upload: 'uploadTestKey'}))
            expect(state).toMatchSnapshot()
            state = UploadReducer(state, UPloadActions.startUpload({upload: 'uploadTestKey'}))
            expect(state).toMatchSnapshot()
            state = UploadReducer(state, UPloadActions.startuploadItem({upload: 'uploadTestKey'}))
            state = UploadReducer(state, UPloadActions.uploadItemSuccess({upload: 'uploadTestKey', id: '12345'}))
            state = UploadReducer(state, UPloadActions.uploadComplete({upload: 'uploadTestKey'}))
            expect(state).toMatchSnapshot()
        })
    })
    describe("mult file upload", () => {
        let currentState = Map({})
        beforeAll(() => {
            ...
        })

        afterAll(() => {
            ...
        })
        ...
        test("handle upload successed", () => {
            ...
        })

        test("handle upload failed", () => {
            ...
        })

        test("hanlde reupload successed", () => {
            ...
        })
    })
})
复制代码

可以看上以上单元测试的问题吗?在这里引入这篇文章所举的例子:

开始测试React Native App(上篇)
笔者就是犯了以上错误,测试语法学会后,不知道如何写测试用例,傻傻的在单元测试里写入集成测试,就会出现如果 reducer 增加了新的 action

处理,那测试文件中应该添加多少个测试用例呢? 于是笔者改成了以下写法:

describe("upload component reducer test", () => {
    it('register upload action will register a upload queue to state', () => {
        let currentState = Map({})
        currentState = UploadReducer(currentState, UPloadActions.registerUpload({upload: 'uploadTestKey'}))
        expect(currentState).toMatchSnapshot()
    })

    it('destroy upload action will remove upload queue from state', () => {
        let currentState = Map({
            'uploadTestKey': new Upload({
                name: 'uploadTestKey'
            })
        })
        currentState = UploadReducer(currentState, UPloadActions.destroyUpload({upload: 'uploadTestKey'}))
        expect(currentState).toMatchSnapshot()
    })

    it('push upload item action will add an uploadItem into upload\'s wattingQueue', () => {
        ...
    })

    it('delete upload item action will remove an uploadItem from upload\'s all queue', () => {
       ...
    })
    ...
})
复制代码

reducer 能处理多少个 action 就有多少个测试用例,是不是明了多了? 示例代码

Redux中的Action Creator测试

Reducer 同样的道理,也是要注意两点,一个是测试用例的职责要对,一定要记住它是“单元测试”,我们只需要保证单个 Action creator 有特定的输入就有特定的输出,而且要对第一次运行测试用例的输出 snapshot 进行检查,保证期望值的正确性。 示例代码

如何测试异步Action

通常的 Action 是一个 Object 对象,带有 type 属性即可,但是 异步Action 它返回的不是一个 Object 而是一个特殊的 Function ,需要类似于 redux-thunk 的中间件来处理。因此我们在测 异步Action 时需要 Mock 两个模块,一个是网络异步所需要的 fetch ,另一个就是可以派发 Async ActionStore

请先阅读Jest官方的Mock相关文档:Mock Functions、 manual-mocks

Mock fetch可以使用库: jest-fetch-mock Mock store可以使用库: redux-mock-store 具体配置查看官方README, 这是 配置好的项目。 Object 类型的 Action 测试写法:

it('register upload action' , () => {
  store.dispatch(UploadActions.registerUpload({upload: 'uploadKey'}))
  expect(store.getActions()).toMatchSnapshot()
})
复制代码

异步Action 测试写法:

it('upload one file fail action test', () => {
  fetch.mockResponseOnce(JSON.stringify({ error: new Error('fail') }))

  return store.dispatch(UploadActions.upload('uploadKey', config))
          .then(() => {
            expect(store.getActions()).toMatchSnapshot()
          })
})
复制代码

异步测试有多种写法,分别用来处理 callBackPromiseasync/await ,具体请查阅官方文档。

Component测试

上面详细讲述了关于Redux的单元测试,下面来看看Component如何做单元测试。

请先阅读 Testing React Native with the new Jest — Part I

需要注意的是,网上有许多文章在写组件测试的时候都使用了 react-native-mock ,用来mock RN的库,但是在RN0.37版本开始,内置于react-native的Jest设置自带 一些 应用于react-native库的mock。可以在 setup.js 中查阅,因此不需要再引入react-native-mock。

Component 测试的核心点:

  • 给不同的props 会有不同的 Dom 输出。
  • 使用 主动执行实例方法 来模拟 State 的变化输出不同的 Dom
  • 测试使用 connect(component) 包裹的组件时,mock connect 组件连接的 props 直接测试被 connect 包裹的组件
  • 测试使用 HOC 的组件时, 分别测试 ComponentWrapComponent

注意上面列表加粗的文字,这些文字就是我们写 Component 测试的着手点。

UI Render测试,我们测试的是不同的 props 有不同的 Dom

it('render login screen with init state', () => {
    const loginWrap = shallow(
        <LoginScreen
            handleSubmit={handleSubmit}
            valid={false}
            submitting={false}
        />
    )
    expect(toJson(loginWrap)).toMatchSnapshot()
})
复制代码

在上段的代码中,我们可以改变 valid 这些属性值,然后使用 toMatchSnapshot 来保留snap。这里涉及的库有: enzyme , enzyme-to-json ,知识点有: shallow

enzyme是使用 javascript 语言为 react 写的测试工具,可以用来快速的获取 Component 的输出( Dom ),操控 Dom ,以及对 Dom 写各种断言。类似的有React Test Utilities和 react-testing-library ,React Test Utilities是React官方出的测试工具,也可以输出 Dom ,但是它不能操作 Dom ,没有提供 Selector 。react-testing-library与enzyme的功能很接近,但是不支持 react-native ,支持 react

enzyme-to-json 可以将 shallow 的结果 json 化输出,一般配合 JesttoMatchSnapshot 使用。 Shallow 的render方式是浅渲染,只生成Dom树的一层,例如:

//ComponentA.js
import React from 'react'
import {
    Text,
    View,
} from 'react-native'

class ComponentA extends React.Component {
    render() {
        return (
            <View><ComponentB /></View>
        )
    }
}
class ComponentB extends React.Component {
    render() {
        return (
            <Text>Hello world</Text>
        )
    }
}

export default ComponentA
复制代码
//ComponentA.test.js
import ComponentA from './ComponentA'
import React from 'react'
import { shallow } from 'enzyme'
import toJson from 'enzyme-to-json'

it('shallow ComponentA', () => {
    const wrap = shallow(<ComponentA/>)
    expect(toJson(wrap)).toMatchSnapshot()
})
复制代码
//ComponentA.test.js.snap
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`shallow ComponentA 1`] = `
<Component>
  <ComponentB />
</Component>
`;
复制代码

使用 Shallow 的渲染结果就是 <View><ComponentB/></View> ,它不会再把 ComponentB 展开获得 <View><Text>Hello world</Text></View> 这种结果。这样我们就不用关心子组件的行为,我们之要专心测 ComponentA 即可。

enzymeenzyme-to-json 的安装,参考官网:airbnb.io/enzyme/

UI交互测试,我们需要主动调用实例方法来触发 state 的更改:

//Foo.js
import React from 'react'
import {
    Switch
} from 'react-native'

export default class extends React.Component {
    constructor() {
        super(...arguments)

        this.state = {
            value: false
        }
    }

    _onChange = (value) => {
        this.setState({value: value})
    }

    render() {
        return (
            <Switch onValueChange={this._onChange} value={this.state.value}/>
        )
    }
}
复制代码
//Foo.test.js
import Foo from './Foo'

import React from 'react'
import { shallow } from 'enzyme'
import toJson from 'enzyme-to-json'

it('Foo change state', () => {
    const wrap = shallow(<Foo/>)
    expect(wrap.state(['value'])).toEqual(false)
    expect(toJson(wrap)).toMatchSnapshot()

    const firstWrap = wrap.first()
    firstWrap.props().onValueChange(true)
    expect(wrap.state(['value'])).toEqual(true)
    expect(toJson(wrap)).toMatchSnapshot()
})
复制代码
//Foo.test.js.snap
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`Foo change state 1`] = `
<Switch
  disabled={false}
  onValueChange={[Function]}
  value={false}
/>
`;

exports[`Foo change state 2`] = `
<Switch
  disabled={false}
  onValueChange={[Function]}
  value={true}
/>
`;
复制代码

在这个例子中,在 firstWrap.props().onValueChange(true) 前分别打印了 snap ,并且断言 state.value 的值,来测试 onValueChange 引起的 state 的更改。 firstWrap.props().onValueChange(true) 就是主动调用实例方法的行为。

HOC测试:

在以上的两个例子中,可以掌握常规组件的单元测试,那么Hoc组件如何测试呢?其实实现方式也很简单,我们把 HOC 拆开来看,可以分别测 Higher OrderComponentComponent 的测试和上两个例子一样,需要注意的是,要分别导出 Higher OrderComponent 以及 HOC :

//Hoc.js
import React from 'react'
import {
    View
} from 'react-native'

export function fetchAble(WrappedComponent) {
    return class extends React.Component{
        _fetchData = () => {
            console.log('start fetch')
        }

        render() {
            return (
                <WrappedComponent fetchData={this._fetchData}/>
            )
        }
    }
}

export class Com extends React.Component {
    render() {
        return (<ComponentA/>)
    }
}

export default fetchAble(View)
复制代码
//Hoc.test.js
import {fetchAble} from './Hoc'
it('Hoc test', () => {
    const A = (props) => <View/>
    const B = fetchAble(A)
    const fetchWarp = shallow(<B/>)

    const wrapA = fetchWarp.find(A)
    expect(wrapA).not.toBeUndefined()
    expect(wrapA.props().fetchData).not.toBeUndefined()
    wrapA.props().fetchData()
    expect(console.log.mock.calls.length).toEqual(1)
    expect(console.log.mock.calls[0][0]).toEqual('start fetch')
})
复制代码

setupJest 中配置了mock console

Redux Connect与HOC是同样的道理

组件测试的参考文章(搭梯子):

Sharing and Testing Code in React with Higher Order Components

Testing React Component’s State

Unit Testing Redux Connected Components

这一篇主要是围绕组件和Redux写单元测试,下一篇将开始写集成以及e2e测试

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


以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

Android和PHP开发最佳实践

Android和PHP开发最佳实践

黄隽实 / 机械工业出版社华章公司 / 2013-3-20 / 79.00元

本书是国内第一本同时讲述Android客户端开发和PHP服务端开发的经典著作。 本书以一个完整的微博应用项目实例为主线,由浅入深地讲解了Android客户端开发和PHP服务端开发的思路和技巧。从前期的产品设计、架构设计,到客户端和服务端的编码实现,再到性能测试和系统优化,以及最后的打包发布,完整地介绍了移动互联网应用开发的过程。同时,本书也介绍了Android系统中比较有特色的功能,比如Go......一起来看看 《Android和PHP开发最佳实践》 这本书的介绍吧!

JS 压缩/解压工具
JS 压缩/解压工具

在线压缩/解压 JS 代码

RGB转16进制工具
RGB转16进制工具

RGB HEX 互转工具

图片转BASE64编码
图片转BASE64编码

在线图片转Base64编码工具