概述
所谓单元测试,就是对每个单元进行的测试,一般针对的是函数、类或单个组件,不涉及系统和集成,单元测试是软件测试的基础测试,一个完备的软件系统都会涉及到单元测试。
目前,Javascript的测试工具很多,但是针对React的测试主要使用的是Facebook推出的Jest框架,Jest是基于Jasmine的JavaScript测试框架,具有上手容易、快速、可靠的特点,是React.js默认的单元测试框架。相比其他的测试框架,Jest具有如下的一些特点:
- 适应性:Jest是模块化、可扩展和可配置的;
- 沙箱和快速:Jest虚拟化了JavaScript的环境,能模拟浏览器,并且并行执行;
- 快照测试:Jest能够对React 树进行快照或别的序列化数值快速编写测试,提供快速更新的用户体验;
- 支持异步代码测试:支持promises和async/await;
- 自动生成静态分析结果:不仅显示测试用例执行结果,也显示语句、分支、函数等覆盖率。
环境搭建
安装Jest
首先,在项目目录下使用下面的命令安装Jest。
npm install --save-dev jest
//或者
yarn add --dev jest
如果你使用的是react-native init命令行方式来创建的RN项目,且RN版本在0.38以上,则无需手动安装,系统在生成项目的时候会自动添加依赖。
"scripts": {
"test": "jest"
},
"jest": {
"preset": "react-native"
}
配置Babel
现在很多的项目都使用es6及以上版本编写,为了兼容老版本,我们可以使用Babel来将es5的语法转换为es6。使用Babel前,我们需要使用如下的命令来安装Babel。
yarn add --dev babel-jest babel-core regenerator-runtime
说明:如果使用的是Babel 的version 7则需要安装babel-jest, babel-core@^7.0.0-bridge.0 和 @babel/core,安全命令如下:
yarn add --dev babel-jest babel-core@^7.0.0-bridge.0 @babel/core regenerator-runtime
然后在项目的根目录里添加 .babelrc 文件,在文件中配置如下react-native脚本内容。
{
"presets": ["react-native"],
"sourceMaps":true // 用于对齐堆栈,精准的定位单元测试中的问题
}
如果是自动生成的, .babelrc 文件的配置脚本如下:
{
"presets": ["module:metro-react-native-babel-preset"]
}
此时,需要将上面的presets配置修改为 “presets”: [“react-native”]。
完整配置
为了方便查看, 下面是package.json文件的完整配置:
{
"name": "jestTest",
"version": "0.0.1",
"private": true,
"scripts": {
"start": "node node_modules/react-native/local-cli/cli.js start",
"test": "jest"
},
"dependencies": {
"react-native": "0.55.4",
"react": "^16.6.0",
"react-dom": "^16.6.0"
},
"devDependencies": {
"babel-core": "^6.26.3",
"babel-jest": "^23.6.0",
"jest": "23.6.0",
"metro-react-native-babel-preset": "0.48.3",
"react-test-renderer": "16.5.0",
"regenerator-runtime": "^0.12.1"
},
"jest": {
"preset": "react-native",
"transform": {
"^.+\\.js$": "babel-jest"
},
"transformIgnorePatterns": [
"node_modules/(?!(react-native)/)"
]
}
}
说明:如果报AccessibilityInfo错误,请注意react-naitve的版本号,因为react-naitve的版本和其他库存在一些兼容问题,请使用0.55.4及以下稳定版本。
Cannot find module 'AccessibilityInfo' (While processing preset: "/Users/xiangzhihong029/Documents/rn/jestTest/node_modules/react-native/Libraries/react-native/react-native-implementation.js")
Enzyme
Enzyme 是 Airbnb 公司开源的测试工具库,是react-addons-test-utils的封装的产品,它模拟了 jQuery 的 API,非常直观并且易于使用和学习,提供了一些与众不同的接口和几个方法来减少测试的样板代码,方便你判断、操纵和遍历 React Components 的输出,并且减少了测试代码和实现代码之间的耦合。相比react-addons-test-utils,enzyme的API 就一目了然,下表是两个框架常用的函数的对比。
Enzyme提供了三种渲染方法:
shallow
shallow 方法就是对官方的 Shallow Rendering 的封装,浅渲染在将一个组件作为一个单元进行测试的时候非常有用,可以确保你的测试不会去间接断言子组件的行为。shallow 方法只会渲染出组件的第一层 DOM 结构,其嵌套的子组件不会被渲染出来,从而使得渲染的效率更高,单元测试的速度也会更快。例如:
import { shallow } from 'enzyme'
describe('Enzyme Shallow', () => {
it('App should have three <Todo /> components', () => {
const app = shallow(<App />)
expect(app.find('Todo')).to.have.length(3)
})
}
mount
mount 方法则会将 React 组件渲染为真实的 DOM 节点,特别是在依赖真实的 DOM 结构必须存在的情况下,比如说按钮的点击事件。
完全的 DOM 渲染需要在全局范围内提供完整的 DOM API, 这也就意味着它必须在至少“看起来像”浏览器环境的环境中运行,如果不想在浏览器中运行测试,推荐使用 mount 的方法是依赖于一个名为 jsdom 的库,它本质上是一个完全在 JavaScript 中实现的 headless 浏览器。
mount渲染方式的示例如下:
import { mount } from 'enzyme'
describe('Enzyme Mount', () => {
it('should delete Todo when click button', () => {
const app = mount(<App />)
const todoLength = app.find('li').length
app.find('button.delete').at(0).simulate('click')
expect(app.find('li').length).to.equal(todoLength - 1)
})
})
render
render 方法则会将 React 组件渲染成静态的 HTML 字符串,返回的是一个 Cheerio 实例对象,采用的是一个第三方的 HTML 解析库 Cheerio。这个 CheerioWrapper 可以用于分析最终结果的 HTML 代码结构,它的 API 跟 shallow 和 mount 方法的 API 都保持基本一致。
import { render } from 'enzyme'
describe('Enzyme Render', () => {
it('Todo item should not have todo-done class', () => {
const app = render(<App />)
expect(app.find('.todo-done').length).to.equal(0)
expect(app.contains(<div className="todo" />)).to.equal(true)
})
})
Jest单元测试
简单示例
首先,我们在项目的根目录新建一个名为__test__的文件夹,然后编写一个组件,例如:
import React, {Component} from 'react';
import {
Text, View,
} from 'react-native';
export default class JestTest extends Component{
render() {
return(<View />)
}
}
然后,我们在__test__文件夹下编写一个名为jest.test.js的文件,代码如下:
import React from 'react';
import JestTest from '../src/JestTest';
import renderer from 'react-test-renderer';
test('renders correctly', () => {
const tree = renderer.create(<JestTest/>).toJSON();
expect(tree).toMatchSnapshot();
});
使用命令 “yarn jest” ,系统就会开始执行单元测试,如果没有任何错误,将会显示PASS相关的信息。
当然,上面的例子并没有涉及到任何的业务逻辑,只是介绍了下在React Native中如何使用Jest进行单元测试。
生成快照测试
快照测试是第一次运行测试的时候在不同情况下的渲染结果(挂载前)保存的一份快照文件,后面每次再运行快照测试时,都会和第一次的比较,除非使用“npm test – -u”命令重新生成快照文件。
为了测试快照测试,我们先新建一个带有逻辑的组件。例如:
import React, {Component} from 'react';
import {
Text, View,
Button
} from 'react-native';
export default class JestTest extends Component{
constructor() {
super();
this.state = {liked: false};
this.handleClick = this.handleClick.bind(this);
}
handleClick() {
return this.setState({
liked: !this.state.liked
});
}
render() {
const text = this.state.liked ? 'like' : 'not liked';
return (<Text onClick={this.handleClick}>
You {text} this.Click to toggle.
</Text>);
}
}
上面的组件拥有三种状态,初始状态,点击状态,以及再次被点击的状态,所以在测试文件中,我们分别生成三种状态的快照,快照测试文件的代码如下:
import React from 'react';
import renderer from 'react-test-renderer';
import JestTest from "../src/JestTest";
describe('<JestTest/>', () => {
it('Snapshot', () => {
const component = renderer.create(<JestTest/>);
let snapshot = component.toJSON();
expect(snapshot).toMatchSnapshot();
snapshot.props.onClick();
snapshot = component.toJSON();
expect(snapshot).toMatchSnapshot();
snapshot.props.onClick();
snapshot = component.toJSON();
expect(snapshot).toMatchSnapshot()
});
});
然后,在控制台运行yarn jest命令,就会看到在__tests___snapshots_\目录下看到快照测试,快照测试文件的代码如下:
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`<JestTest/> Snapshot 1`] = `
<Text
accessible={true}
allowFontScaling={true}
ellipsizeMode="tail"
onClick={[Function]}
>
You
not liked
this.Click to toggle.
</Text>
`;
exports[`<JestTest/> Snapshot 2`] = `
<Text
accessible={true}
allowFontScaling={true}
ellipsizeMode="tail"
onClick={[Function]}
>
You
like
this.Click to toggle.
</Text>
`;
exports[`<JestTest/> Snapshot 3`] = `
<Text
accessible={true}
allowFontScaling={true}
ellipsizeMode="tail"
onClick={[Function]}
>
You
not liked
this.Click to toggle.
</Text>
`;
如果需要更新快照文件,执行yarn test – -u命令。
DOM测试
DOM测试主要测试组件生成的 DOM 节点是否符合预期,比如响应事件之后,组件的属性与状态是否符合预期。DOM 测试 依赖于官方的 TestUtil,所以需要安装react-addons-test-utils依赖库,安装的时候注意版本的兼容问题。不过在实战过程中,我发现react-addons-test-utils会报很多错误,并且官方文档也不是很友好。
这里推荐使用airbnb开源的Enzyme 脚手架,Enzyme是由 airbnb 开发的React单测工具,它扩展了React的TestUtils,并通过支持类似jQuery的find语法可以很方便的对render出来的结果做各种断言,开发体检十分友好。
生成测试报告
使用命令yarn test – --coverage就可以生成测试覆盖报告。如图:
同时,还会在根目录生成一个名为 coverage 的文件夹,是测试覆盖报告的网页版,包含更多,更详细的信息。
Jest基础语法
匹配器
匹配器用于测试输入输出的值是否符合预期,下面介绍一些常见的匹配器。
普通匹配器
最简单的测试值的方法就是看值是否精确匹配,使用的是toBe(),例如:
test('two plus two is four', () => {
expect(2 + 2).toBe(4);
});
toBe()使用的是JavaScript中的Object.is(),属于ES6中的特性,所以不能检测对象,如果要检测对象的值的话,需要用到toEqual。
test('object assignment', () => {
const data = {one: 1};
data['two'] = 2;
expect(data).toEqual({one: 1, two: 2});
});
Truthiness
在实际的测试中,有时候我们需要明确区分undefined、null和false等情况,而Jest提供的下面的一些规则可以帮我们完成上面的需求。
- toBeNull只匹配null
- toBeUndefined只匹配undefined
- toBeDefine与toBeUndefined相反
- toBeTruthy匹配任何if语句为真
- toBeFalsy匹配任何if语句为假
数字匹配器
toBeGreaterThan():大于
toBeGreaterThanOrEqual():大于或者等于
toBeLessThan():小于
toBeLessThanOrEqual():小于或等于