从echarts-for-react源码中学习如何写单元测试
前言
如果你熟悉React
和Echarts
的话,应该有用到过 echarts-for-react(虽然它现在没有维护了),本文就通过它写的测试用例来学习下如何写单元测试
如何测试function
有如下函数,作用是「浅复制obj中的keys」,如何判断它返回的是期待的结果?
const pick = (obj, keys) => { // 浅复制obj中的keys
const r = {};
keys.forEach((key) => {
r[key] = obj[key];
});
return r;
};
测试用例
// 浅复制obj中的keys
import { pick } from '../src/utils';
// 把遇到的计时器挂起,在必要时,再使用jest.runOnlyPendingTimers执行掉已经挂起的计时器
jest.useFakeTimers();
// 描述块,将多个test用例包裹在一起
describe('utils.js', () => {
// test即it
test('pick', () => {
// 期望值
// 当执行pick函数后,希望它的返回值符合我的期望
expect(pick({ a: 1 }, [])).toEqual({});
expect(pick({ a: 1 }, ['b'])).toEqual({});
expect(pick({ a: 1 }, ['a'])).toEqual({ a: 1 });
expect(pick({ a: 1 }, ['a', 'b'])).toEqual({ a: 1 });
});
});
分析
① jest.useFakeTimers()
作用:
把遇到的计时器挂起,在必要时,再使用jest.runOnlyPendingTimers
执行掉已经挂起的计时器
这里使用jest.useFakeTimers()
的目的就是暂停正在执行的timer
,防止这些timer
影响到下面的测试用例。(但是我没看出来哪里的timer影响到了,有知道的同学望告知)
② 使用expect(A).toEqual(B)
,判断A
的返回值与B
相等
注意:
toEqual()
的作用是 判断值相等即可,即使是两个对象,但它们的值是一样的,也是可以的
小结
对于有返回值的function
,就是通过判断「返回值」,是否与「期望值」相等即可
这样的好处: ① 当有新需求要扩展该函数时,可以保证该函数的返回值仍保持不变,进而不会影响到使用到该函数的旧需求
② 当测试的函数比较复杂时,非常方便,不用了解内部的详细代码,只需返回值符合期望即可
如何测试ReactComponent
当我写完一个React组件时,我该如何测试它呢?
测试用例
import React from 'react';
//enzyme库用来判断、操纵和遍历 ReactComponents
import {mount} from 'enzyme';
import EchartsReact from '../src';
import option from './option';
describe('echarts-for-react', () => {
// 测试react component
test('react component', () => {
// mount()借助jsdom模拟浏览器环境,并提供DOM api和生命周期的支持,方便测试HOC(高阶组件)
// shallow()浅渲染,将组件渲染成虚拟DOM对象,不会渲染内部子组件,也无法与子组件互动
// render()用于将React组件渲染成静态的HTML并分析生成的HTML结构
// 渲染一个react组件
const component = mount(<EchartsReact
option={option}
className="echarts-for-react-root"
/>);
// 判断 组件是否存在
expect(component.exists()).toBe(true);
// 判断是否 只有一层div嵌套
// find()会递归遍历所有子节点
expect(component.find('div').length).toBe(1);
});
});
分析
① 使用enzyme.mount()
生成完整的React组件
② mount()
/shallow()
/render()
的区别如下:
[1] mount()
借助jsdom
模拟浏览器环境,并提供DOM api
和生命周期
的支持,方便测试HOC
(高阶组件)
[2] shallow()
浅渲染,将组件渲染成虚拟DOM
对象,它不会渲染内部子组件,也无法与子组件互动
[3] render()
用于将React
组件渲染成静态的HTML
并分析生成的HTML
结构
③ toEqual()
和toBe()
的区别
[1] toEqual()
只要求值相等,即使是不同的对象,只要值相等即可
const a={}
const b={}
expect(a).toEqual(b); //test passed
[2] toBe()
不仅要求值相等,还要求object指向同一内存地址
const a={}
const b=a
expect(a).toEqual(b); //test passed
const c={}
const d={}
expect(c).toEqual(d); //test failed
④ component.find()
会递归遍历自身及所有子节点
如何测试DOM节点上的属性
测试用例
test('compoent dom node', () => {
// 渲染一个react组件
const component = mount(<EchartsReact
option={option}
className="echarts-for-react-root"
/>);
// root tag
// 获取最外层节点,判断节点名是否为div
// getDOMNode() 获取DOM节点
expect(component.getDOMNode().nodeName.toLowerCase()).toBe('div');
// class name
// 获取最外层节点,判断类名是否为 xxx
expect(component.getDOMNode().className).toBe('echarts-for-react echarts-for-react-root');
// style
// 获取最外层节点,判断height是否为 300px
expect(component.getDOMNode().style.height).toBe('300px');
})
调用getDOMNode()
即可
如何测试React组件实例
测试用例
test('component instance', () => {
const component = mount(<EchartsReact
className="cls"
option={option}
/>);
// echarts instance, id 以 ec_ 开头,如:ec_1595664672003
expect(component.instance().getEchartsInstance().id.substring(0, 3)).toBe('ec_');
});
调用instance()
即可获取React组件实例,也就是ref
属性
如何测试组件上的props
测试用例
test('component props', () => {
// jest.fn()建立 mock function
// 进行单元测试时,应该将关注点放在「测试目标」上,onChartReady 作为被依赖的function,
// 内部发生了什么与「测试目标」无关,只需关注返回的值(return xxx)即可,
// 不能因为 onChartReady 而影响到「测试目标」,为了减少依赖,就使用了 mock function 即 jest.fn()
// 参考:https://medium.com/enjoy-life-enjoy-coding/jest-jojo%E6%98%AF%E4%BD%A0-%E6%88%91%E7%9A%84%E6%9B%BF%E8%BA%AB%E8%83%BD%E5%8A%9B%E6%98%AF-mock-4de73596ea6e
const testOnChartReadyFunc = jest.fn();
const testFunc = () => {
};
// not default props
const component = mount(<EchartsReact
option={option}
style={{width: 100}}
lazyUpdate
theme="test_theme"
onChartReady={testOnChartReadyFunc}
onEvents={{onClick: testFunc}}
/>);
// 测试实例的props是否符合预期
expect(component.props().option).toEqual(option);
expect(component.props().style).toEqual({width: 100});
expect(component.props().className).toBe('');
expect(component.props().lazyUpdate).toBe(true);
expect(component.props().theme).toBe('test_theme');
expect(typeof component.props().onChartReady).toBe('function');
expect(component.props().onEvents).toEqual({onClick: testFunc});
// 判断 testOnChartReadyFunc 被调用
expect(testOnChartReadyFunc).toBeCalled();
// udpate props 更新传入的props
component.setProps({
className: 'test-classname',
style: {height: 500},
});
component.update(); // force update
expect(component.props().style).toEqual({height: 500});
expect(component.getDOMNode().style.height).toBe('500px');
expect(component.props().className).toBe('test-classname');
});
分析
① jest.fn()
作用:
新建mock function
在进行单元测试时,应该将关注点放在「测试目标」上,而onChartReady
作为被依赖的function
,不管它的内部发生了什么,都与「测试目标」无关,只需关注返回的值(return xxx
)即可
为了减少依赖,所以使用了mock function
即jest.fn()
② 通过component.props()
获取到传到组件上的props
③ 通过expect(function).toBeCalled()
,判断函数有被调用
④ 通过component.setProps()
,来为组件传入新属性
⑤ 通过component.update()
来强制更新React组件,如果组件是ClassComponent
的话,会调用里面的生命周期
如何测试组件已卸载
测试用例
test('unmount', () => {
const component = mount(<EchartsReact
option={option}
className="cls"
/>);
// 注销组件
component.unmount();
expect(() => {
// 组件注销后是获取不到实例的,所以判断是 toThrow() 抛出错误
component.instance();
}).toThrow();
});
通过component.unmount()
卸载组件后,再去获取组件的instance
,这时候肯定是获取不到,会报错的,所以通过toThrow()
来抛出错误,从而让test
顺利pass
其他API
https://enzymejs.github.io/enzyme/docs/api/mount.html
通过本文,你应该知道
① jest.useFakeTimers()
的作用及何时使用
② 如何测试function
③ 如何测试ReactComponent
④ mount()
/shallow()
/render()
的区别
⑤ toEqual()
和toBe()
的区别
⑥ 如何测试DOM
节点上的属性
⑦ 如何测试React
组件实例上的属性
⑧ 如何测试组件上的props
⑨ jest.fn()
的作用
⑩ 如何测试组件已卸载
源码地址(有改动)
https://github.com/AttackXiaoJinJin/echarts-for-react/tree/webchen/__ tests __
- 不被未来折叠掉:AI时代下的思考
- Google 分析的基准化测试
- WebFont 三宗罪之一:WebFont 与 FOUT
- 探究基于声明的身份标识
- 深化“互联网+先进制造业”发展工业互联网的系列解读二:打造平台体系
- WebFont 三宗罪之二:吹毛求疵的WebFont 渲染差异
- IDC发布IT转型报告,现代化、自动化、转型三要素必不可少
- 存储过程和触发器的应用
- 两部委印发车联网产业标准体系建设指南 提到了自动驾驶
- 状态开关按钮ToggleButton
- 微软开放 .NET 框架源代码
- Angularjs基础(十二)
- 妙趣横生的HTML5 Page Visibility API
- 禁止/移除 WordPress 4.2 中前台自动加载的 emjo 脚本
- JavaScript 教程
- JavaScript 编辑工具
- JavaScript 与HTML
- JavaScript 与Java
- JavaScript 数据结构
- JavaScript 基本数据类型
- JavaScript 特殊数据类型
- JavaScript 运算符
- JavaScript typeof 运算符
- JavaScript 表达式
- JavaScript 类型转换
- JavaScript 基本语法
- JavaScript 注释
- Javascript 基本处理流程
- Javascript 选择结构
- Javascript if 语句
- Javascript if 语句的嵌套
- Javascript switch 语句
- Javascript 循环结构
- Javascript 循环结构实例
- Javascript 跳转语句
- Javascript 控制语句总结
- Javascript 函数介绍
- Javascript 函数的定义
- Javascript 函数调用
- Javascript 几种特殊的函数
- JavaScript 内置函数简介
- Javascript eval() 函数
- Javascript isFinite() 函数
- Javascript isNaN() 函数
- parseInt() 与 parseFloat()
- escape() 与 unescape()
- Javascript 字符串介绍
- Javascript length属性
- javascript 字符串函数
- Javascript 日期对象简介
- Javascript 日期对象用途
- Date 对象属性和方法
- Javascript 数组是什么
- Javascript 创建数组
- Javascript 数组赋值与取值
- Javascript 数组属性和方法
- 基于Tensorflow2 Lite在Android手机上实现图像分类
- Python知乎专栏爬虫,pdfkit专栏文章制作PDF电子书
- Python爬虫练手,一个简单的Python资讯采集案例
- Python与seo,百度关键词相关搜索关键词采集源码
- istio请求路由分析
- [剑指]2空格替换
- (补充)SPAN+AVISPA for Verifying Cryptographic Protocols
- 如何修改终端中$符号前面的一串名称?
- Alfred 有多强悍,我写了个一键上传图片的 workflow 来告诉你
- Peewee出错:Illegal mix of collations
- 【TOOLS】Linux环境下升级python2.7到python3.6
- 溯源黑帽利用 Web 编辑器漏洞非法植入 SEO 页面事件
- Milvus 查询合并机制
- Django model 层之Models与Mysql数据库小结
- 【010期】JavaSE面试题(十):集合之Map18连环炮!