单元测试
什么是单元测试?
- 单元测试是用来对一个模块、一个函数或者一个类来进行正确性检验的测试工作,这里的单元是程序工作的最小工作单位,单元测试应该仅仅依赖输入,不依赖多余的环境.
为什么要写单元测试?
- 减少缺陷率
- 是很好的“文档”,代码重构的基础
什么时候写单元测试?
- TDD 测试驱动开发
- BDD 行为驱动开发
什么代码需要写单元测试?
- 逻辑复杂的
- 容易出错的
- 不易理解的
- 公共代码
- 核心业务功能
1. 单元测试框架:jest
- jest的特点
- 易用性:基于Jasmine,提供断言库,支持多种测试风格
- 适应性:Jest是模块化、可扩展和可配置的
- 快速:Jest内置了JSDOM,能够模拟浏览器环境,并且并行执行
- 快照测试:Jest能够对React组件树进行序列化,生成对应的字符串快照,通过比较字符串提供高性能的UI检测
- Mock系统:Jest实现了一个强大的Mock系统,支持自动和手动mock
- 支持异步代码测试:支持Promise和async/await
- 自动生成静态分析结果:内置Istanbul,测试代码覆盖率,并生成对应的报告
- 安装jest
- yarn add --dev jest/ npm install --dev jest
2. enzyme
- jest+Enzyme 是目前比较流行的React项目单元测试组合
- 是由 airbnb 开发的React单测工具,它模拟了jQuery的API
- yarn add --dev enzyme enzyme-adapter-react-16
- enzyme配合react16使用的初始化配置:
// 使用enzyme还需要根据React的版本安装适配器
import Enzyme from "enzyme";
import Adapter from "enzyme-adapter-react-16";
Enzyme.configure({
adapter: new Adapter()
});
jest的常用配置:
// https://jestjs.io/docs/zh-Hans/configuration#testmatch-array-string
"jest": {
// 指定需要进行单元测试的文件匹配规则
"testMatch": ["<rootDir>/src/**/*.test.js"],
// 测试启动文件
"setupFiles": [
"<rootDir>/setup/jest.setup.js",
"<rootDir>/setup/enzyme.setup.js"
],
// 单元测试文件检测后缀名
"moduleFileExtensions": ['js', 'jsx'],
// 需要忽略的文件匹配规则
"testPathIgnorePatterns": ["src/node_modules/"],
"collectCoverage": false,
//设置阈值
"coverageThreshold": {
"global": {
"statements": 60,
"functions": 50,
"branches": 40
}
},
"coverageDirectory": "<rootDir>/coverage",
"coverageReporters": [
"cobertura",
"html",
"lcov"
],
//层次显示测试套件中每个测试的结果。
"verbose": false
},
运行:npm run test / yarn test
3. 测试基本分为三步
- describe: 定义一个测试套件
- test/it:定义一个测试用例
- expect:断言的判断条件 toEqual:断言的比较结果
4. 钩子函数
- beforeAll() 会在所有测试用例之前执行一次
- afterAll() 会在所有测试用例之后执行一次
- beforeEach() 会在每个测试用例之前执行
- afterEach() 会在每个测试用例之后执行
- 注:describe的after函数优先级要高于全局的after函数,describe的before函数优先级要低于全局的before函数
5. jest对象
-
jest.fn() 创建Mock函数最简单的方式,如果没有定义函数内部的实现,jest.fn()会返回undefined作为返回值。
-
jest.spyOn() 返回一个mockfunction,和jest.fn相似,但是能够追踪调用信息
-
jest.mock() 用来mock一些模块或者文件
jest.mock('src/index', () => ({
getStore: jest.fn(() => ({
getState: jest.fn(() => ({})),
dispatch: jest.fn(),
})),
}))
jest.mock('src/xxx/xx, () => ({
show: jest.fn(),
showShortCenter: jest.fn(),
}))
* 总结
- jest.fn()常被用来进行某些有回调函数的测试
- jest.mock()可以mock整个模块中的方法,节约测试时间和测试的冗余度是十分必要
- jest.spyOn()需要测试某些必须被完整执行的方法时
6. 快照测试
- 需要引入react-test-renderer库,使用其中的renderer方法
快照可以测试到组件的渲染结果是否与上一次生成的快照一致;
toMatchSnapshot方法会帮助我们对比这次将要生成的结构与上次的区别;
安装: npm i -D babel-preset-react react-test-renderer
- 例子
//例如:
render() {
const { width, height, direction } = this.props
if (direction === 'horizontal') {
return <View style={[{ backgroundColor: this.props.lineColor, height }, this.props.style]} />
}
return <View style={{ backgroundColor: this.props.lineColor, width }} />
}
//快照test文件
import React from 'react'
import renderer from 'react-test-renderer'
import Line from './Line'
describe('Line snapshot', () => {
test('should render Line correctly', () => {
const tree = renderer.create(<Line />).toJSON()
expect(tree).toMatchSnapshot()
})
test('should render Line correctly when direction is vertical', () => {
const tree = renderer.create(<Line direction="vertical" width={3} />).toJSON()
expect(tree).toMatchSnapshot()
})
test('should render Line correctly when direction is horizontal', () => {
const tree = renderer.create(<Line direction="horizontal" height={3} />).toJSON()
expect(tree).toMatchSnapshot()
})
})
7. 异步测试
- jest支持对异步的测试,支持Promise和Async/Await两种方式的异步测试
8. 常用的断言库
- toBe(value): 比较数字、字符串
- toEqual(value): 比较对象、数组
- toBeNull(): 只匹配null
- toBeUndefined(): 只匹配undefined
- toBeDefined(value):与toBeUndefined相反
- toMatch(regexpOrString):用来检查字符串是否匹配,可以是正则表达式或者字符串
- not:用来取反
- toHaveLength(): 有多少个
用来判断mock function是否被调用过
- toHaveBeenCalledWith()
- toHaveBeenCalled()
9. enzyme的渲染方法
- shallow 浅渲染,只渲染当前组件并将组件渲染成虚拟DOM对象,只能能对当前组件做断言
- render 静态渲染,渲染成静态的HTML字符串
- mount 完全渲染,它将组件渲染加载成一个真实的DOM节点,用来测试DOM API的交互和组件的生命周期,当前组件以及所有子组件,耗时更长,内存占用的更多
常用方法
- simulate(event,mock):模拟事件,用来触发事件,event为事件名称,mock为一个event object
- instance():返回组件的实例
- find(selector):根据选择器查找节点,selector可以是CSS中的选择器,或者是组件的构造函数,组件的display name等
- at(index):返回一个渲染过的对象
- get(index):返回一个react node,要测试它,需要重新渲染
- contains(nodeOrNodes):当前对象是否包含参数重点 node,参数类型为react对象或对象数组
- text():返回当前组件的文本内容
- html(): 返回当前组件的HTML代码形式
- props():返回根组件的所有属性
- state():返回根组件的状态
- setState(nextState):设置根组件的状态
- setProps(nextProps):设置根组件的属性
10. 组件测试
- 工具类函数测试
export const isHPurchase = (type) => {
return type === ‘HPurchase’
}
//test文件
describe('test type is isHPurchase', () => {
test.each([
['HPurchase', true],
['h_REDEMPTION', false],
['', false],
[null, false],
[undefined, false],
[11, false],
['PURCHASE', false],
])('should return %s when type is %s', (type, expected) => {
expect(isHPurchase(type)).toEqual(expected)
})
})
- 组件类测试
// 组件
render() {
const { heightStyle, noBottomBorder } = this.props
return (
<TouchableWithoutFeedback onPress={this.handleOnClick}>
<View style={styles.container}>
<View style={[styles.ItemView, heightStyle.height !== null && heightStyle]}>
<View style={styles.left}>{this.renderLeft()}</View>
<View style={{ flex: 1, marginRight: 10 }}>{this.renderRight()}</View>
</View>
{!noBottomBorder && <View style={[styles.line, { marginLeft: 20 }]} />}
</View>
</TouchableWithoutFeedback>
)
}
//测试文件
import { shallow } from 'enzyme'
import { View, TouchableWithoutFeedback } from 'react-native'
import Toast from 'src/modules/Toast'
import { UserInfoItem } from './UserInfoItem'
describe('UserInfoItem', () => {
test('UserInfoItem', () => {
const renderRight = jest.fn()
const renderLeft = jest.fn()
const props = {
isEditable: false,
noBottomBorder: true,
renderRight,
renderLeft,
}
const component = shallow(<UserInfoItem {...props} />)
component.find(TouchableWithoutFeedback).simulate('press')
expect(component.find(TouchableWithoutFeedback)).toHaveLength(1)
expect(Toast.showShortCenter).toBeCalledWith('该信息不可编辑')
expect(component.find(View)).toHaveLength(4)
})
})
11. 代码覆盖率
- 行覆盖率(line coverage):是否测试用例的每一行都执行了
- 函数覆盖率(function coverage):是否测试用例的每一个函数都调用了
- 分支覆盖率(branch coverage):是否测试用例的每个if代码块都执行了
- 语句覆盖率(statement coverage):是否测试用例的每个语句都执行了