单元测试
1 React适合单元测试
- 组件化
- Functional Component
- 单向数据流
2 通用测试框架-Jest
- 支持多平台,运行速度快
- 内置代码覆盖率测试
- 为 React 提供了一些特殊的测试方法
2.1 断言库
- 判断一个值是否对应相应的结果
- https://jestjs.io/docs/en/using-matchers
1、在要测试的js目录下新建一个js文件,如在src文件夹下新建example.test.js
test('test equal', () => {
expect(2 + 2).toBe(4)
// 判断2+2等于4是否正确,结果正确
expect(2 + 2).toBe(3)
// 判断2+2等于3是否正确,结果报错
})
test('test not equal', () => {
expect(2 + 2).not.toBe(5)
// 判断2+2不等于5是否正确,结果正确
})
test('test to be true or false', () => {
expect(1).toBeTruthy()
expect(0).toBeFalsy()
// 期望1为真,0为假
})
test('test number', () => {
expect(4).toBeGeaterThan(3)
expect(4).toBeLessThan(5)
// 判断4大于3是否正确,结果正确
// 判断4小于5是否正确,结果正确
})
test('test object', () => {
expect({name:'Tom',age: 30}).toEqual({name:'Tom',age: 30})
// 判断两个项目的属性值是否一样,结果正确
})
运行js:npm test src/example.test.js
3 React测试工具
- React官方测试工具 - ReactTestUtils(使用复杂)
- Airbnb基于官方的封装 - Enzyme(使用更简便)
3.1 Enzyme的两种测试方法
- Shallow Rendering
- DOM Rendering
具体使用有官方文档可以查看
3.2 简单测试代码
1、安装:npm install enzyme-adapter-react-16 --save-dev
2、在src文件夹下创建一个setupTests.js文件
import { configure } from 'enzyme'
import Adapter from 'enzyme-adapter-react-16'
configure({ adapter: new Adapter() })
// 配置文件
3、在需要测试的组件文件夹下创建一个TotalPrice.test.js
import React from 'react'
import { Shallow } from 'enzyme'
import TotalPrice from '../TotalPrice'
// TotalPrice是需要测试的组件
const props = {
income: 1000,
outcome: 2000
}
// 期望组件显示收入 1000,支出 2000,incom&outcome是TotalPrice中的描述方法
describe('test TotalPrice component', () => {
it('component should render correct incom&outcome number', () => {
const wrapper = shallow(<TotalPrice {...props}/>)
expect(wrapper.find('.income span').text() * 1).toEqual(1000)
expect(wrapper.find('.outcome span').text() * 1).toEqual(2000)
})
})
4、运行:npm test src/components/_test_/TotalPrice.test.js
3.3 价格列表单元测试分析
- 传入特定数组,是否渲染对应条目
- 每个条目是否渲染特定组件和内容
- 点击按钮是否触发特定回调
3.3.1 TDD(了解)
TDD是测试驱动开发(Test-Driven Development)的英文简称,是敏捷开发中的一项核心实践和技术,也是一种设计方法论。
TDD的原理是在开发功能代码之前,先编写单元测试用例代码,测试代码确定需要编写什么产品代码。
TDD的重要目的不仅仅是测试软件,测试工作保证代码质量仅仅是其中一部分,而且是在开发过程中帮助客户和程序员去除模棱两可的需求。
TDD首先考虑使用需求(对象、功能、过程、接口等),主要是编写测试用例框架对功能的过程和接口进行设计,而测试框架可以持续进行验证。
优点:在任意一个开发节点都可以拿出一个可以使用,含少量bug并具一定功能和能够发布的产品。
缺点:增加代码量。测试代码是系统代码的两倍或更多,但是同时节省了调试程序及挑错时间。
3.3.2 Snapshot testing
- Jest 为 React 测试提供的特性
- 为价格列表添加 snapshot
snapshot就是组件前后照片对比,在这里如果列表改动前后又变动,就会被检测出来,如果是特地改动的,就可以更新snapshot
1、新建PriceList.test.js
import React from 'react'
import { shallow } from 'enzyme'
import PriceList from '../PriceList'
import { item, categories } from '../../containers/Home'
// 引入列表数据
const itemsWithCategory = item.map(item => {
item.category = categories[item.cid]
return item
})
const props = {
items: itemsWithCategory,
onModifyItem: jest.fn(),
onDeleteItem: jest.fn()
}
let wrapper
describe('test PriceList component', () => {
// beforeEach钩子可以使每次运行测试用例之前都会执行这一步
beforeEach(() => {
wrapper = shallow(<PriceList {...props}/>)
})
it('should render the component to match snapshot', () => {
//在这里如果列表改动前后又变动,就会被检测出来,如果是特地改动的,就可以更新snapshot
expect(wrapper).toMatchSnapshot()
})
it('should render correct price items length', () => {
//传入特定数组,是否渲染对应条目
expect(wrapper.find('.list-group-item').length).toEqual(itemsWithCategory.length)
})
it('should render correct icon and price for each item', () => {
//每个条目是否渲染特定组件和内容
//不想写了,太具体针对特例了
})
it('should trigger the correct function callbacks', () => {
//点击按钮是否触发特定回调
const firstItem = wrapper.find('.list-group-item').first()
// simulate('click') 模拟点击事件,这里是指点击第一个a标签(编辑按钮)
firstItem.find('a').first().simulate('click')
expect(props.onModifyItem).toHaveBeenCalleWith(itemsWithCategory[0])
// 这里是指点击最后一个a标签(删除按钮)
firstItem.find('a').last().simulate('click')
expect(props.onDeleteItem).toHaveBeenCalleWith(itemsWithCategory[0])
})
})
2、运行:npm test src/components/_test_/PriceList.test.js
3.4 首页单元测试分析
- 测试默认状态 - 是否正确渲染特定组件和数据 等
- 测试交互 - 点击交互该组件的 state 是否有相应的修改
- 测试交互 - 对应操作触发后展示型组件的属性是否修改
1、新建测试文件
import React from 'react'
import { mount } from 'enzyme'
import Home, { items } from '../Home'
import { LIST_VIEW, CHART_VIEW, TYPE_INCOME}
import PriceList from '../../components/PriceList'
import ViewTab from '../../components/ViewTab'
import MonthPicker from '../../components/MonthPicker'
import CreateBtn from '../../components/CreateBtn'
let wrapper
describe('test Home container component', () => {
beforeEach(() => {
wrapper = mount(<Home />)
})
it('should render the defalut layout', () => {
const currentDate = paresToYearAndMonth('2018/10/01')
expect(wrapper.find(PriceList).length).toEqual(1)
expect(wrapper.find(ViewTab).props().activeTab).toEqual(LIST_VIEW)
expect(wrapper.find(MonthPicker).props().year).toEqual(currentDate.year)
expect(wrapper.find(MonthPicker).props().month).toEqual(currentDate.month)
})
})