closertb / closertb.github.io

浏览issue 或 我的网站,即可查看我的所有博客
https://closertb.site
32 stars 0 forks source link

怎样写一个具有异步交互的React组件的单元测试 #26

Open closertb opened 4 years ago

closertb commented 4 years ago

写于:2018-09-28

关于前端React组件测试(jest,Enzyme),网上有大量的入门文章,可以看看,但如果你确实想了解前端自动化测试,个人更推荐看官方的文档和一些比较官方的测试案列,这里推荐两个:

  1. enzyme官方文档,涵盖了各种说明和API;
  2. jest官方文档,涵盖了各种说明和API;
  3. antd基础组件库,每个组件都有较丰富的测试用例; 本篇文章适合对前端组件测试有一定概念的同学,本文将包含以下几点:
    • shallow, mount, render三种方法渲染的区别;
    • 组件事件的模拟及事件回调的mock;
    • 异步事件的模拟;

      三种方法渲染的区别

      组件测试最重要的前提,就是你需要知道怎么实例化自己的组件,然后才能去判断是否渲染正常,交互怎么样,功能是不是都OK,而这就和下面要说的渲染方法相关,了解一下三种方法的区别,有助于自己少掉坑,少抠脑壳,少掉头发,用一个关于antd Select组件的示例来说明。

      function RenderTest() {
      return (
      <div className="render">
      <Select>
        <Option key="1" value="1">test1</Option>
        <Option key="2" value="2">test2</Option>
        <Option key="3" value="3">test3</Option>
      </Select>
      </div>
      );
      }
      describe('base render test', () => {
      it('component mounted right', () => {
      const wrapper = mount(
      <RenderTest />
      );
      console.log('***mount***',
      'Select:', wrapper.find('Select').length,
      '  Option:', wrapper.find('Option').length,
      '   Div', wrapper.find('div').length,
      '   class', wrapper.find('.render').length);
      });
      });

      在浏览器中,挂载这个RenderTest,渲染出来的DomTree是这样的: image 然后在组件测试中分别用了三种方法来渲染,得到是下面的结果: image

    • Shallow:与语义一样,肤浅表面的,也是浅渲染, 大概就是组件长啥样,就渲染成啥样,子组件不会递归渲染;
    • Mount: 又称Full DOM rendering,组件层虚拟Dom与真实Dom的渲染,所以这个方法里你既能看到Select节点(为2与组件的定义有关),Option为0,因为Antd的Option都是以绝对定位的方式挂载在body节点下的,而非wrapper节点里。你还可以打印wrapper.find('Trigger')的长度,结果为1,至于为什么,和Select为2一样,需要去看Select的源码;
    • Render: 又称静态渲染,真实dom节点的渲染,无虚拟Dom,不过有趣的是wrapper节点就是根节点,所以wrapper.find('.render')的length为0,而且很多前两者能用的方法在这里都没有,比如containsMatchingElement这样最基本的方法。

组件事件的模拟及事件回调的mock

下面两节都将以自己最近封装的一个Antd组件为例来做说明,在我前面的一篇文章里有提到,如下图所示: 组件实例演示

这个组件的大致功能如上面图所示,产品需求就是需要一个编辑框,这个框在用户点击输入时,需要弹出一个搜索框,根据用户的输入远程搜索获取数据形成一个下拉列表,供用户选择,选择完成后搜索框被收起。而从代码上,也很简单:

<div
  className="originSearch"
  style={style}
  ref={el => this.searchInputWrapper = el}
>
  <Input
    ref={e => this.searchInput = e}
    readOnly
    placeholder={placeholder}
    value={valueFormat(value)}
    style={{ width: '100%' }}
    size="default"
    {...inputProps}
  />
  {
    isShowSearch &&
    <div className="js-origin-search origin-search">
      <Icon type="search" className="origin-search-icon" />
      <AutoComplete
        autoFocus
        ref={el => this.searchRealInput = el}
        className="certain-category-search"
        dropdownClassName="certain-category-search-dropdown"
        dropdownMatchSelectWidth
        onSearch={this.handleChange}
        onSelect={this.handleSelect}
        style={{ width: '100%' }}
        optionLabelProp="value"
      >
        {loading ? [<Option key="loading" disabled><Spin spinning={loading} style={{ paddingLeft: '45%', textAlign: 'center' }} /></Option>] : options}
      </AutoComplete>
    </div>
  }
</div>

第一个事件,用户开始输入,Input被单击,click事件捕捉,isShowSearch由false变为true, AutoComplete组件渲染,并自动获得焦点。 对于这一个测试,这是需要有用户交互的,和测试点击浏览器,所以我们需要用到模拟事件simulate,看下面代码:

it('Input state change disable when click', () => {
  const wrapper = mount(
    <HInputSearch {...searchProps} />
  );
  wrapper.find('input').simulate('click');
  expect(wrapper.state('isShowSearch')).toEqual(true);
  const inputNodes = wrapper.find('input');
  expect(inputNodes.length).toEqual(2);
});

除了模拟点击事件,还可以模拟输入框值的change事件,接着我们还可以检测这个变化是否触发了相应的方法,比如下面这段:

it('Input state change disable when click', () => {
  const inputValue = 'change';
  const change = jest.spyOn(HInputSearch.prototype, 'handleChange'); // handleChange是在定义组件时,定义的一个原型方法
  const wrapper = mount(
    <HInputSearch {...searchProps} />
  );
  wrapper.find('input').simulate('click');
  expect(wrapper.state('isShowSearch')).toEqual(true);
  const inputNode = wrapper.find('.origin-search input');
  inputNode.simulate('change', { target: { value: inputValue } });
  expect(wrapper.find('input').get(1).props.value).toEqual(inputValue);
  expect(change).toBeCalledWith(inputValue); // 这里可以用toBeCalled检测是否调用,而使用toBeCalledWith除了检测是否调用,还可以检测是否正确的传参;
});  

除了在原型上直接mock响应的方法,也可以直接在实例上,查找出某个节点利用jest.spyOn来检测某个方法是否被调用。在下一节还会继续对jest的函数mock进行说明。

异步请求的模拟

此次封装的案例组件我称之为远程搜索输入框,所以涉及到防抖与异步请求的发起,所以在Input框值变化时,首先是使用lodash的debounce函数防抖,然后发起请求。所以当我们进行触发后的流程测试时,比如异步请求是否被调用,返回值是否正常的被存入state,Option是否生成,这些统统没法立即执行测试,而是需要一段时间的等待再来判断,我们把这称之为异步测试。进行这个测试,先理一理思路: