前言
如果你熟悉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(<EchartsReactoption={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(<EchartsReactoption={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(<EchartsReactclassName="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(<EchartsReactoption={option}style={{width: 100}}lazyUpdatetheme="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(<EchartsReactoption={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 __
(完)