• React 单元测试入门


    在这里插入图片描述

    结合之前编写组件库的一些单元测试经验尝试总结一篇文章帮助刚入门的小伙伴少走一些坑。

    前言

    不知道大家有没有看到过,一些网上流传的关于程序员写bug的段子:

    众所周知,程序员的工作是写bug
    –微博网友

    我是一个斐波那契程序员,我每天都在修前天和昨天写的 bug
    —Twitter

    今天我们来聊聊怎么跳出这个斐波那契循环,通过单元测试的方式减少程序中bug的数量。注意一下用词是减少,不是消除,单测不是银弹,过分信任单测而不做集成测试可能导致如下后果:
    在这里插入图片描述

    一、关于为什么要做单元测试

    很多同学不太理解为什么要做单元测试(上一个这样想的波音的外包同学已经把波音737搞坠毁了),在日常开发过程中,我们对待编写测试用例的态度大概是这样的:

    1. 业务这么忙,怎么可能有时间写单测?(可能下个以及下下个需求可能就是修复线上bug)
    2. 单测有什么用?把写单测的时间用来开发业务逻辑,效率提升美滋滋(于是你成了斐波那契程序员)
    3. 我写的代码天下第一,没有bug,不需要单测。(可以认为没有测试过的代码就是有问题的代码)

    强调了一下单元测试的重要性,然后讲讲什么情况下要写单测,写单测要因情况而变:
    ● 公用组件库,底层SDK,基础部分程序,更新不频繁,一定要编写测试用例,且一般要求95%以上到100%。
    ● 业务项目,通常变化过快,编写成本过高,可以通过CR + 测试全量回归替代单测功能。

    写单测有很多好处,比方说:
    ● 减少Bug,通过严格的测试用例保证代码逻辑正确。
    ● 减少修复bug的成本,方便隔离定位bug。
    ● 提高重构的成功率,如果有单测存在,那么就不用太担心改爆历史代码,放心做技改。

    我们做技改的第一件事情就是把覆盖率提升到了99%。
    ● 提高开发速度,减少后期解决bug的时间,反过来整体提升开发速度。

    ● 写单测确实要花费很大的心血,尤其是很多时候我们需要保持高覆盖率的时候,但对于那些底层的需要长期维护的项目和代码而言,写单元测试是很值得的。

    二、不要迷信测试覆盖率

    我们看开源库是否可用的一个重要标准就是该项目是否有严格的测试用例,没有单测的项目基本可以认为不靠谱。通常我们会看单元测试的覆盖率,针对代码的测试覆盖率也有很多种度量方式,常见的比方说行覆盖率,分支覆盖率,函数覆盖率,路径覆盖等等。

    项目概要中列给用户看的是行覆盖率,也是最容易达到的覆盖率指标。注意以下一些观点:
    ● 覆盖率数据只能代表你测试过哪些代码,不能代表你是否测试好这些代码,尤其UI层的交互非常难以测试。
    ● 不要过于相信覆盖率数据,100%的测试覆盖率并不能保证bug的不出现。
    ● 代码覆盖率只是一个最基本的前提,一定要保证,但不是意味着达到指标就代表测试的完成。
    ● 测试人员不能盲目追求代码覆盖率,而应该想办法设计更多更好的案例,哪怕多设计出来的案例对覆盖率一点影响也没有。

    列一些项目的测试覆盖率:

    (1) Egg: 100%

    chair团队的结晶,100%的覆盖

    在这里插入图片描述

    (2) Ant Design : 96%

    部门扛鼎之作,基础组件库

    在这里插入图片描述
    (3) TechUI: 97%

    核心组件库,要求大家保证90%以上的覆盖率。

    三、了解测试框架 Jest

    Jest 是 Facebook 发布的一个开源的、基于 Jasmine 框架的 JavaScript 单元测试工具。提供了包括内置的测试环境 DOM API 支持、断言库、Mock 库等,还包含了 Snapshot Testing、 Instant Feedback 等特性。它自动集成了断言、JSDom、覆盖率报告等开发者所需要的所有测试工具,是一款几乎零配置的测试框架,并且它对同样是 Facebook 的开源前端框架 React 的测试十分友好。

    在这里插入图片描述
    如上面的知识图谱所示,一个常见的测试框架通常需要实现这些功能:
    ● before/after 钩子函数: 如beforeEach,afterEach,
    ● Mock方法: 函数Mock,时间mock等。
    ● 断言: 判断一个描述是否正确,在Jest中常为 expect(xxx).toBe(xxx) 的形式
    ● 测试覆盖率:提供多种形式的测试报告,如HTML,文本等形式

    这些基本的测试功能是每一个测试框架都需要考虑实现的,各个测试框架除了Api的设计方式可能有所不同之外,其核心功能是一致的,在使用新的测试框架的时候你仍然可以通过上面的脑图按图索骥,抛开Api的表象,快速上手使用。

    四、Enzyme介绍

    Enzyme是Airbnb开源的 React 测试类库,它提供了一套简洁强大的 API,并通过 jQuery 风格的方式进行DOM 处理,开发体验十分友好。不仅在开源社区有超高人气,同时也获得了React 官方的推荐。

    我们可以认为,Enzyme是专门为React打造的用于方便测试React组件的库,必须搭配一样测试运行器来使用,Enzyme在英文中是催化剂的意思,通常我们配合Jest来使用,让他们产生化学反应。

    Enzyme提供了三种不同级别的渲染方式,分别对应三个方法,这三个方法会返回 wrapper 对象,类似 JQuery 的$ 变量,我们可以通过 wrapper 提供的 API 对 DOM 元素进行操作和遍历。

    Shallow Rendering: 浅渲染

    在单元测试的过程中,浅渲染将一个组件渲染成虚拟 DOM 对象,并不会渲染其内部的子组件,也不是真正完整的React Render,无法与子组件互动,可以有效防止组件功能的测试被子组件影响。

    import { shallow } from 'enzyme';
    ...
    const wrapper = shallow(<MyComponent />);
    
    • 1
    • 2
    • 3

    Full DOM Rendering 完全渲染

    完全渲染适合用来测试和DOM api打交道的组件或者高阶组件,full render会在一个类浏览器的环境中(jsdom)挂载。

    注意: 和shallow以及static渲染不同的是,完全渲染会将组件真正挂载到DOM节点中去,因此不同的测试之间可能会相互影响,因此有必要在每次测试后通过unmount方法清理组件。

    import { mount } from 'enzyme';
    ...
    const wrapper = mount(<Foo />);
    
    wrapper.unmount();
    
    • 1
    • 2
    • 3
    • 4
    • 5

    Static Rendered Markup 静态渲染

    静态渲染会使用cheerio 来解析生成组件的HTML结构,返回cheerio包裹对象,之后你可以使用cheerio提供方法来操作dom结构

    import { render } from 'enzyme';
    ...
    const wrapper = render(<Foo />);
    expect(wrapper.find('.foo-bar')).to.have.lengthOf(3);
    
    • 1
    • 2
    • 3
    • 4

    五、基本使用

    上面的方法使用之后返回wrapper类,提供各种测试Api。可以看到下面的代码中我们有一些 Adapter 这样的配置代码,使用Bigfish的话,内置的 umi-test 已经帮你做好这些事情了,这些配置代码可以省略。

    1. Debug 打印组件树

    开始测试组件的首要步骤,我们可以通过 warpper.debug() 打印渲染出来的组件,可以很方便地找到要交互的组件名称。

    import React from 'react'
    import App from './App'
    import { configure, shallow } from 'enzyme'
    import Adapter from 'enzyme-adapter-react-16'
    
    configure({ adapter: new Adapter() })
    
    describe(`<App />`, () => {
      it('should render App', () => {
        const warpper = shallow(<App />)
        console.log(warpper.debug())
      })
    })
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    2. 函数调用Mock与事件模拟

    在测试过程中我们常用的一个测试方法就是创建一个模拟的函数挂载到按钮的处理器上,然后点击按钮,观察函数是否被调用,使用 jest 和 Enzyme 我们可以很方便地做到这些:

    const clickFn = jest.fn(); // 使用fn()创建模拟函数
    describe('MyComponent', () => {
      it('button click should hide component', () => {
        const component = shallow(<MyComponent onClick={clickFn} />);
        component
          .find('button#my-button-two')
          .simulate('click'); // simulate方法模拟点击
        expect(clickFn).toHaveBeenCalled(); // 判断函数被调用,以及被什么参数调用都可以
      });
    });
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    3. 快照测试

    顾名思义,快照测试就是将本次测试的DOM结构和上次快照下来的DOM结构进行比较的测试。

    之前在开发的时候我们会经常发现dom结构变更导致snappet测试挂掉比较麻烦,所以有同学质疑为什么要进行Snapshot测试?

    实际上当UI库处在快速迭代中的时候,UI和功能不断在修改,所以觉得snapshot会比较麻烦,毕竟每次都要更新,不过一旦UI稳定之后快照测试的作用就体现了,“每当你想要确保你的UI不会有意外的改变,快照测试是非常有用的工具”,比方说,我们的组件都依赖了X文件,其他人在开发中修改了X文件,导致Y组件的UI发生了变化,我们就能够感知到。

    一个小技巧是当组件的API Review和实现都稳定后再添加快照测试,这样既不用频繁更新,又可以享受快照测试的遍历。

     it('should match snapshot', () => {
    	const wrapper = mount(<DateRanger selects={[DateRanger.THIS_YEAR]} />);
    	// 快照一致
    	expect(toJson(wrapper)).toMatchSnapshot();
    	wrapper.unmount();
    });
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    六、实战演练 - 函数替换 & 异步函数测试

    在这里插入图片描述

    下面两个案例的代码都在这个组件中,这个组件中使用debounce方法进行请求函数去抖,发送请求后通过Promise异步获取请求结果更新组件。

    案例一.Debounce 方法的测试

    (1)遇到的问题

    import debounce from 'lodash/debounce';
    //...
    this.fetchData = debounce(this.fetchUserData, this.props.delayTime);
    
    • 1
    • 2
    • 3

    代码中用debounce函数防抖控制请求的发送频率,那么在测试用例中怎么去让debounce过的函数被立即执行呢?

    (2)解决方案

    使用 jest 的 mock 方法对 debounce 函数进行函数替换:

    beforeEach(() => {
      jest.useFakeTimers();
      debounce.mockImplementation(fn => fn); // 测试执行前mock函数
    });
    
    afterEach(() => {
      jest.useRealTimers();
      debounce.mockRestore(); // 执行后恢复
    });
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    fn => fn 的含义是指返回这个函数本身,比方说: this.fetchData = debounce(this.fetchUserData, this.props.delayTime); 运行完debounce函数后,就会变成 this.fetchData === this.fetchUserData

    (3)完整代码

    import * as React from 'react';
    // eslint-disable-next-line import/no-extraneous-dependencies
    import { mount } from 'enzyme';
    import debounce from 'lodash/debounce';
    import UserSearch from '../index';
    
    jest.mock('lodash/debounce');
    
    describe('<UserSearch />', () => {
      beforeEach(() => {
        jest.useFakeTimers();
        debounce.mockImplementation(fn => fn);
      });
    
      afterEach(() => {
        jest.useRealTimers();
        debounce.mockRestore();
      });
    	// 可以看一下如何测试一个用户搜索过程
      it('test debounce function', done => {
        const wrapper = mount(<UserSearch userSearchRequest={userSearchRequest} />);
    
        wrapper.find('input').simulate('change', { target: { value: '1' } });
        jest.runAllTimers();
        wrapper.update(); // 强制进行组件更新
    
        // show spin loading...
        expect(wrapper.find('Spin')).toHaveLength(1); // 输入之后首先会转菊花
    
        process.nextTick(() => { // 后面讲,让异步方法执行
          wrapper.update();
          expect(wrapper.find('MenuItem').length).toBe(userList.length); // 组件显示和userList一样长度的用户候选项
          // finish search
          wrapper.find('.ant-select').simulate('blur'); // 触发 blur 不选择
          jest.runAllTimers();
          wrapper.update();
          // empty text input
          expect(wrapper.find('input').text()).toBe(''); // input值为空
          wrapper.unmount();
          done();
        });
      });
    });
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43

    案例二. 测试返回promise的异步方法

    (1)遇到的问题

    异步方法通常都具有不确定性,promise的方法也是,但是我们希望在函数调用后立即能够测试到组件的执行效果,怎么办呢?比方说我的promise方法是这样:

    fetchData = query => {
      const { userSearchRequest } = this.props;
      userSearchRequest &&
        userSearchRequest({ query })
          .then(users => {
            ...
            dataSource = ...;
            this._mounted && this.setState({ dataSource, fetching: false });
          })
          .catch(err => {
            this._mounted && this.setState({ fetching: false });
          });
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    那么这个then什么时候执行?执行完之后 setState 怎么看到结果?

    (2)解决方案

    使用 process.nextTick() 方法,nextTick 会在目前队列中的所有事件完成之后执行,保证了promise方法此时已经完成执行,注意一下测试异步方法需要使用 done() 回调。

    (3) 样例代码

    it('should stop fetching if users empty', done => { // 注意这里的done
      const wrapper = mount(
        <UserSearch userSearchRequest={() => Promise.resolve([])} />,
      );
    
      wrapper.find('input').simulate('change', { target: { value: '1' } });
      jest.runAllTimers();
    
      process.nextTick(() => { // 完成本事件循环之后,Promise就执行结束了
        wrapper.update();
        expect(wrapper.instance().state.fetching).toBe(false);
        done(); // 一定要调用 done 表示测试执行完毕
      });
    });
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    七、如何快速编写测试用例?

    编写测试代码的难度并不大,可能还没有下决心去写测试用例的勇气的难度大。

    写测试用例最关键的大法就是复制粘贴大法,找到相似的场景,复制代码,改吧改吧执行就好:
    在这里插入图片描述
    最好的参考材料就是AntD, Ant Design 组件库本身有非常丰富的组件测试样例,我们开发的业务组件很多都是依赖 Ant Design 基础组件进行的封装,测试用例非常依赖 Ant Design 的组件结构,很多意想不到的测试方式可以在 Ant Design 的测试用例中找到,提高我们的测试编写效率。

    八、配置Jest产生测试覆盖率报告

    在测试过程中,我们需要查看我们的单元测试覆盖了多少代码,查看覆盖率报告,jest也内置了测试覆盖率报告的功能。首先我们添加 jest 配置文件 jest.config.js :

    module.exports = {
      "collectCoverageFrom": [ "**/*.{js,jsx}", "!**/node_modules/**",],
      "collectCoverage": true,
      "coverageReporters": ["html", "text"],
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5

    这样jest在运行单测之后就会生成测试报告了,html的报告默认会放到 coverage 文件夹中,打开其中的 index.html 文件就可以看到测试报告了。

  • 相关阅读:
    YOLO目标检测——交通标志分类数据集【含对应voc、coco和yolo三种格式标签】
    【已解决】vscode 配置C51和MDK环境配置
    Tetrazine-PEG-Aminooxy|四嗪-聚乙二醇-氨甲基|四嗪PEG修饰氨甲基
    maven项目正在idea的创建和配置
    ES6那些不知道的事儿
    AD7606模块
    31岁才转行程序员,目前34了,我来说说我的经历和一些感受吧...
    为什么pca分量没有关联
    linux命令汇总
    RabbitMQ——死信队列
  • 原文地址:https://blog.csdn.net/weixin_43606158/article/details/123883889