谈一谈ReactNative单元测试

3,448 阅读7分钟

前言

2018年,公司成功上市后,要求保证代码的稳定性和健壮性,所有的二方库和业务库都需要进行单元测试,当时上级要求单元测试的代码覆盖率不能少于70%。我当时作为团队的所有ReactNative项目的单元测试的owner,整理了一份单元测试的文档,同时利用单元测试的特点与各成员分工独立开发,最后所有RN项目的覆盖率都在95%以上。刚好最近稍微有点时间,就挑一部分单元测试相关的内容出来跟大家分享下。。。

为什么要做单元测试?

很多人应该都有个疑问,我们先来看几个场景

1.功能测试

问题:开发过程中,一些通用的独立性比较强的功能测试,如果在工程中做集成测试,编译及测试流程相对比较繁琐,而且测试代码一般不能保留在工程里,不利于后续维护或排查问题。

  • 单元测试具有独立的运行环境,可以方便快速的独立 mock 某一模块的各种逻辑,在单测中,可以通过代码覆盖率快速定位到具体哪个函数、哪一行没有覆盖到,从而避免出现某些逻辑被忽视。而且单元测试代码是独立于业务之外的,不影响项目的业务逻辑,因此测试用例可以保留下来,方便后续维护和排查问题。

2. UI 组件测试

问题: 对于某些通用的且不会轻易改动的 UI 组件,比如封装的 Refresh、Alert 等组件。这类组件通常会应用在很多文件或很多个项目里,改动这类组件时,影响范围比较大,一般不能随便修改。

  • 可以通过单元测试中的快照测试(Snapshot)记录当前的 tree,当有影响 UI 组件的代码发生变动时,可以快速准确的定位到该组件是否被修改过,并定位到具体修改了哪一行。如果某组件确实需要被修改,只需要更新对应的快照 tree 即可,方便快捷。

3. 代码可测性

问题:很多时候,当我们看到自己一年前写的代码,不敢相信是自己写的,有一种重构的冲动。很多时候为了赶项目进度等各种原因,导致我们写的代码模块之间关联性太大,独立性不强,导致可维护性较差

  • 单元测试,一般会测试某个文件、某个模块、某个函数、甚至是某一句代码,这个就对代码的模块化和独立性要求比较高,一般单元测试比较全面的代码的可维护性也会更强壮。

使用什么进行单元测试?

Jest+Enzyme 是目前比较流行的 React 项目单测组合。Facebook 官方在 ReactNative 项目中使用的是 Jest+react-test-renderer 的单测组合。

  • 1、Jest 是 Facebook 开发的一个开源的、基于 Jasmine 框架的 JavaScript 单元测试框架,它集成了测试执行器、断言库、spy、mock、snapshot 和测试覆盖率报告等功能。ReactNative 项目本身也是使用 Jest 进行单测的,因此它们俩的契合度相当高。

  • 2、react-test-renderer 使用体验比 Enzyme 会差一点,但是对 reactNative 支持比较友好,也是 Facebook 官方在 ReactNative 项目中推荐并使用的开发框架,建议项目中使用这个框架。

  • 3、Enzyme 是由 airbnb 开发的 React 单测工具。它扩展了 React 的 TestUtils,并通过支持类似 jQuery 的 find 语法可以很方便的对 render 出来的结果做各种断言,开发体检十分友好(注:经实践,发现 Enzyme 主要针对 React 单测,对 ReactNative 单测支持不是很友好,如使用 mount 时会编译报错等,故建议使用 react-test-renderer 进行 UI 相关测试)。

怎么进行单元测试?

配置环境

1. 安装 Jest

yarn add --dev jest
或
npm install --save-dev jest

2. 安装 Enzyme

运行以下指令:

npm i --save-dev enzyme enzyme-adapter-react-16

详见:Installation

2. 安装 react-test-renderer

yarn add --dev react-test-renderer

3. 配置 Babel,支持 ES6 语法

yarn add --dev babel-jest babel-core regenerator-runtime

注:需要在项目的根目录里添加 .babelrc 文件,文件中配置 react-native 的预设

{
  "presets": ["react-native"],
  "sourceMaps": true // 用于对齐堆栈,方便精准的定位单元测试中的问题
}

4. 在 package.json 中添加 jest 的配置

{
  "jest": {
    "setupFiles": ["<rootDir>/__mocks__/setup.js"],
    "collectCoverage": true,
    "collectCoverageFrom": [],
    "coveragePathIgnorePatterns": [],
    "preset": "react-native",
    "transform": {
      "^.+\\.js$": "babel-jest"
    },
    "transformIgnorePatterns": [
      "node_modules/(?!(react-native)/)"
    ],
    "moduleNameMapper": {
      "\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)": "<rootDir>/**mocks**/fileMock.js"
    }
  }
}
  • preset:预设 react-native 环境,配和.babelrc 文件的配置,支持 react-naitve 编译
  • transform:支持 ES6/ES7 语法
  • transformIgnorePatterns:忽略 node_modules 下除了(js-consultkit|react-native|medical-modules)外的文件
  • setupFiles:配置 mock 文件的入口,会在测试用例之前编译
  • collectCoverage:设置是否显示覆盖率
  • collectCoverageFrom:设置需要纳入单测覆盖率的文件, 如 "/ConsultIM/src/**.js"
  • coveragePathIgnorePatterns:设置不需要纳入单测覆盖率的文件
  • moduleNameMapper:代表需要被 Mock 的资源名称。如果需要 Mock 静态资源(如 img、file 等),需要配置 Mock 的路径,如 /mocks/xxMock.js

5. 在项目里添加 setup.js 和 fileMock.js 文件

setup.js 单元测试的初始化入口,会在测试用例之前编译,这里可以引入一些 mock 文件的头文件等

fileMock.js 主要用于 mock 掉资源文件(图片、音频等),可以为空文件

6. 运行 jest,开始单元测试

yarn jest 或 yarn jest --watch

详见:单元测试常用指令集

7. 参考文档

Getting Started

Testing React Native Apps

代码覆盖率

代码覆盖率是一个测试指标,用来描述测试用例的代码是否都被执行。统计代码覆盖率一般要借助代码覆盖工具,Jest集成了Istanbul这个代码覆盖工具。

1.四个测量维度

  • 行覆盖率(line coverage):是否测试用例的每一行都执行了
  • 函数覆盖率(function coverage):是否测试用例的每一个函数都调用了
  • 分支覆盖率(branch coverage):是否测试用例的每个if代码块都执行了
  • 语句覆盖率(statement coverage):是否测试用例的每个语句都执行了

在四个维度中,如果代码书写的很规范,行覆盖率和语句覆盖率应该是一样的。会触发分支覆盖率的情况有很多种,主要有以下几种: ||&&?!,if语句,switch语句

2.设置覆盖率

stanbul可以在命令行中设置各个覆盖率的门槛,然后再检查测试用例是否达标,各个维度是与的关系,只要有一个不达标,就会报错。

{
  ...
  "jest": {
    "coverageThreshold": {
      "global": {
        "branches": 50,
        "functions": 50,
        "lines": 50,
        "statements": 50
      },
      "./src/components/": {
        "branches": 40,
        "statements": 40
      },
    },
    "collectCoverage": true,
    "collectCoverageFrom": ["<rootDir>/src/HealthRecord/Common/**.js"],
    "coveragePathIgnorePatterns": [
       "<rootDir>/src/HealthRecord/Common/HRUtil.js"
    ],
  }
}
    1. 通过collectCoverage 来配置是否需要显示单测覆盖率,开启true时,运行效率会降低,同时会生成对应的html文件,可以查看哪些代码已经被覆盖到
    1. 通过collectCoverageFrom 来配置需要统计单测覆盖率的文件
    1. 通过coveragePathIgnorePatterns 来配置不需要统计单测覆盖率的文件
    1. 通过coverageThreshold 配置项来设置不同测试维度的覆盖率阈值。global是全局配置,默认所有的测试用例都要满足这个配置才能通过测试。还支持通配符模式或者路径配置,如果存在这些配置,那么匹配到的文件的覆盖率将从全局覆盖率的计算中去除,独立使用各自设置的阈值
    1. <rootDir>是项目的根路径

常用的单元测试指令

1. 运行所有 tests

jest

2. 只运行某一个路径下单元测试

jest my-test
jest path/to/my-test.js

也可以在编译器上点击某个单元测试前的三角符号,直接运行某个单元测试

3. 监听模式

jest --watch
jest --watchAll

即只要有代码变动,保存后,就会自动运行单元测试

4. 显示覆盖率

jest --coverage

运行时,会在终端显示覆盖率报告,同时会在项目里生成相应的 html 文件作为覆盖率报告

5. 强制停止其它占用,运行当前测试

jest --detectOpenHandles

运行单元测试时,有时候会提示还有占用当前的单元测试的其它线程,运行该指令,强制停止其它占用,运行当前测试

6. 参考文档

Jest CLI Options


觉得不错请给一个小小的 鼓励一下~