从echarts-for-react源码中学习如何写单元测试

1,172 阅读8分钟

前言

如果你熟悉ReactEcharts的话,应该有用到过 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(03)).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 functionjest.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

enzymejs.github.io/enzyme/docs…

通过本文,你应该知道

jest.useFakeTimers()的作用及何时使用
② 如何测试function
③ 如何测试ReactComponent
mount()/shallow()/render()的区别
toEqual()toBe()的区别
⑥ 如何测试DOM节点上的属性
⑦ 如何测试React组件实例上的属性
⑧ 如何测试组件上的props
jest.fn()的作用
⑩ 如何测试组件已卸载

源码地址(有改动)

github.com/AttackXiaoJ…_ tests _


(完)