17

第十二集: 从零开始实现( jest单元测试 )

1.聊聊测试

    本次我会与大家分享一下我学测试时候记的笔记知识以及本次项目里面做的几个测试.
    前端代码的单元测试与集成测试属于雷声大雨点小, 很多人一提到它都说是个好东西, 试问又有几个公司的vue项目是严格要求跑单元测试与集成测试的那?? 测试没通过是否暂停上线? 除了大公司没有几家做得到吧, 毕竟大多数公司只是让专业的测试团队进行'人肉测试'.
    现在前端体系搞得好庞大, 围绕着前端开发的技术与知识点层出不穷, 更别说各种技术之间那剪不断理还乱的纠葛, 我听有人说过: "我只想好好写前端代码, 其他的不管行不行", 这句话是个病句, 这些杂七杂八的技术也都是前端技术, 如果你只会写你所谓的'前端代码', 那你真的只能是一辈子'初学者'了┑( ̄Д  ̄)┍.
    对于这个人人都说好, 但是人人不咋用是咋回事那??🙅‍♂️接下来我们就他的优缺点进行罗列.

2.优缺点

缺点

  1. 前端的测试技术体系还未成形, 本套ui用的就是vue-cli集成的jest 真心不好用....
  2. 有一定的学习成本, 我面试过很多6年以上经验的, 连'设计模式'都搞不懂, 更别说让他学测试了...
  3. 可有可无的处境, 很多工程没有测试跑的好好的, 写了反而bug多多
  4. 不想进步的人的阻拦, 真别小看这条, 很多技术人员会制造各种理由, 不想跳出舒适区.
  5. 每次改需求或是优化代码, 则都需要改两份代码, 人力消耗大.

优点

  1. 多一种思考维度, 多一门技术护身, 对于要以技术养家的人来说, 这条也很重要.
  2. 为主体逻辑的畅通保驾护航, 整套测试能跑下来就不会有太大的错误
  3. b格高, 让别人看了能放心用你的东西, 这也是硬实力

3.用法与分类

大体上分为两类:

  1. BDD 把所有逻辑都写好, 然后根据你的整体逻辑制定你的测试, 好处当然是好理解,更有整体思维, 缺点就是覆盖率低, 并不是很保险.
  2. TDD 把测试写好再进行开发, 这个模式挺有意思, 先写测试, 也就是在脑中先整体布局, 每一步都是自己思考好了再去做的测试覆盖率可能是100%, 他的缺点就太明显了, 开发人员技术必须硬, 而且如果改需求...有的忙了.

基本搭建
我是在vue项目里面直接选择的jest测试
单独实验的朋友可以自行安装 npm i jest -D
去配置一下

"scripts": {
    "test": "jest --watchAll"
  },

命令行里运行npm run test即可
如果电脑运行说没有这个命令的话, 可以用npx 或者在全局安装一下
如果是es项目的话, 要集成一下 npm i @babel/core @babel/preset-env -D
jest内置了 对babel的依赖, 他看到.babelrc就会去配合解析的

基本使用
jest 会自动查找 xx.test.js的文件, 配置如下
随便修改成你喜欢的语义化就好, element-ui采用的是spec
这面这个文件可以通过, jest init生成
jest.config.js

  testMatch: [
    '**/tests/unit/**/*.(spec|test).(js|jsx|ts|tsx)|**/__tests__/*.(js|jsx|ts|tsx)'
  ],
// 1: 最外层describe相当于一个大的父容器盒子, 把测试进行分'块'
// 在出错的时候, 控制台会报出是哪一'块'出错了
describe('按钮相关代码', () => {
// 2: '小块'测试单元, 具体的某些职责的测试, 
  test('测试 按钮点击小伙', () => {
// 3: 断言, 也就是真正判断某些值是否正确的一步
    expect(1).toBe(1);
  });
});

以偶上述为例

// 意思就是判断, 1 是否 === 1
expect(1).toBe(1);
// 由此可知, expect函数负责接收要测试的值
// toBe则为 所谓的 ===, 与他里面的值进行比较
// 那既然有 === 肯定就会有更多种类型的判断了
// 他学名叫配置器

多种类型的'配置器'

  1. toEqual 并不是 == 严格说他是忽略引用,只比内容,内部估计是做了序列化 所以 {a:1}.toEqual({a:1}) true
  2. toBeFalsy 可否转化为 false
  3. toBeTruthy 可否转化为true
  4. toBeUndefined 是 undefined
  5. toBeDefined 不是 undefined
  6. toBeNull === null
  7. not 翻转修饰符, expect(1).not.toBe(2); 1不是2
  8. toBeGreaterThanOrEqual(3) 大于等于3
  9. toBeLessThanOrEqual(3) 小于等于3
  10. toBeLessThan(3) 小于3
  11. toBeGreaterThan(3) 大于3
  12. 'abc'.toMatch('b') // 是否包含'b'字符串, 可以写正则

生命周期

beforeEach(() => {
  // 每个test执行之前都会执行我
});

afterEach(() => {
// 每个test执行之后都会执行我
});

beforeAll(() => {
// 所有test执行之前执行我
});

afterAll(() => {
//   所有test都执行完执行我
});
describe('按钮相关代码', () => {
  test('测试 按钮点击小伙', () => {
    expect(1).toBe(1);
  });
});

这个时代所有插件的配置都趋于'函数化'
上面的生命周期函数很符合设计模式, 我们在写项目的时候也可以借鉴一下.

看完上面这些是不是感觉测试页很容易, 坑的在后面结合vue项目时.

4.vue里面

vue里面当然天差地别, 渲染方式都不一样了, 这个还好有vue自己团队提供的支持

介绍几个vue里面的概念

  1. mount: 可以理解我vue里面的实例化组件的方法, 官网这么说:'创建一个包含被挂载和渲染的 Vue 组件的 Wrapper', 也就是一个完整的渲染, 他的优点就是完整, 但是缺点也明显就是效率低
  2. shallowMount 和 mount 一样,创建一个包含被挂载和渲染的 Vue 组件的 Wrapper,不同的是被存根的子组件。也就是仅仅挂载当前组件实例;
  3. Wrapper 是一个包括了一个挂载组件或 vnode,以及测试该组件或 vnode 的方法。直观点讲就是专门用于测试的实例

由于篇幅有限, 我就直接拿我工程里面的举例子了;
其实到底要测些什么这方面, 我理解的也不是很透, 所以只是简单的几个例子, 一起学习一起讨论.
按钮组件
按钮的测试
vue-cc-ui/tests/unit/Button.test.js

// shallowMount是@vue/test-utils官方提供的测试工具
import { shallowMount } from '@vue/test-utils';
import Button from '../../src/components/Button';
// 这是参考网上封装的获取dom的方法, 下面会有说明👇
import { findTestWrapper } from '../utils/util';


describe('测试button组件', () => {

  it('1: 可以渲染出button组件', () => {
// 利用shallowMount实例化我的button组件
    const wrapper = shallowMount(Button);
// 关键词contains, 判断 Wrapper 是否包含了一个匹配选择器的元素或组件。
// 也就是我想判断, 这个button组件渲染完毕, 页面上是否真的有一个button元素
    expect(wrapper.contains('button')).toBe(true);
  });

  it('2: button组件点击时会触发click事件', () => {
// 依旧是先渲染
    const wrapper = shallowMount(Button);
    // 找到button实例, 这里的at(0), 类似数组的[0];
    const button = findTestWrapper(wrapper,'button').at(0);
    // 在button身上触发其click方法
    button.trigger('click');
    // emitted : 返回一个包含由 Wrapper vm 触发的自定义事件的对象。
    // 也就是监听是否页面里面出发了 this.$emit('click')事件
    // toBeTruthy 这个我们👆上面讲过了
    expect(wrapper.emitted().click).toBeTruthy();
  });

  it('3: 传入icon参数, 可以显示icon组件', () => {
   // shallowMount初始化时, 可以传递参数进去
// 下面的操作大家都懂
    const wrapper = shallowMount(Button,{
      propsData:{
        icon:'cc-up'
      }
    });
    // 找到和这个icon元素
    const icon = findTestWrapper(wrapper,'icon').at(0);
    // 在我传递了icon之后, 这个icon组件必须存在
    expect(icon).toBeTruthy();
  });

});

上面的例子里面提到了一个公共方法我来解释一下

export const findTestWrapper = (wrapper, tag) => {
    return wrapper.findAll(`[data-test="${tag}"]`);
  };
  

我们在书写代码的时候, 为了方便以后的测试, 也会添加一些测试属性, 比如下面这种

<div data-test='name'>
  {{name}}
</div>

取值:

findTestWrapper(wrapper,'name')

findAll 是 wrapper身上的方法, 与之对应还有find 只找寻一个

输入框的测试

import { shallowMount } from '@vue/test-utils';
import Input from '../../src/components/Input';
import { findTestWrapper } from '../utils/util';


describe('测试button组件', () => {

  it('1: 可以渲染出Input组件', () => {
// 这个属于基础步骤了
    const wrapper = shallowMount(Input);
    expect(wrapper.contains('input')).toBe(true);
  });

  it('2: 输入value与显示的内容相同, 并且修改联动', () => {
// 测试是否双向绑定
    const wrapper = shallowMount(Input,{
        propsData:{
            value:'内容1'
        }
    });
    // 取到输入框实例
    const input = findTestWrapper(wrapper,'input').at(0);
    // element就是直接取到dom了...这个dom也是未dom
    // value可以模拟的拿出显示的值
    expect(input.element.value).toBe('内容1')
    // 改变也随之改变
    wrapper.setProps({ value: '内容2' })
    // 只要一起变了就满足需求
    expect(input.element.value).toBe('内容2')
  });

// 我的输入框是有清除功能的额
  it('3: 清除内容按钮有效', () => {
    const wrapper = shallowMount(Input,{
        propsData:{
            value:'内容1',
            clear:true
        }
    });
    // hover 时候才会出现!!
    // 这是组件的内部触发条件, setData可以强行改变组件内部的data数据
    wrapper.setData({
        hovering:true
    })
    const clear = findTestWrapper(wrapper,'clear').at(0);
    // 这里也讲过toBeTruthy可以判断是否可转true
    // 也就是这个定义的实例是否存在
    expect(clear).toBeTruthy();
    // 触发清除事件
    clear.trigger('click');

    expect(wrapper.emitted().input).toBeTruthy();
  });

  it('4: 传入icon参数, 可以显示icon组件', () => {
    const wrapper = shallowMount(Input,{
      propsData:{
        icon:'cc-up'
      }
    });
    const icon = findTestWrapper(wrapper,'icon').at(0);
    expect(icon).toBeTruthy();
  });

  it('5: 切换type, 出现文本框', () => {
    const wrapper = shallowMount(Input,{
      propsData:{
        type:'textarea'
      }
    });
    const textarea = findTestWrapper(wrapper,'textarea').at(0);
    expect(textarea).toBeTruthy();
  });

});

测试分页器

import { shallowMount } from '@vue/test-utils';
import Pagination from '../../src/components/Pagination';
import { findTestWrapper } from '../utils/util';

describe('测试分页器组件', () => {
  it('1: 可以渲染出分页器组件', () => {
    const wrapper = shallowMount(Pagination,{
        propsData:{
            pageTotal:5,
            value:1
        }
    });
    // classes  返回 Wrapper DOM 节点的 class。返回 class 名称的数组。或在提供 class 名的时候返回一个布尔值。这个的意思就是 这个dom的class 是 'cc-pagination'
    expect(wrapper.classes()).toContain('cc-pagination');
  });

  it('2: 传入1000页是否显示1000页', () => {
    const wrapper = shallowMount(Pagination, {
        propsData:{
            pageTotal:1000,
            pageSize:1000,
            value:1
        }
    });
    const li = findTestWrapper(wrapper, 'item');
    // 这个元素我获取到了1000个
    expect(li.length).toBe(1000);
  });

  it('3: 点击第三页是否跳转到第三页', () => {
    const wrapper = shallowMount(Pagination, {
        propsData:{
            pageTotal:10,
            pageSize:10,
            value:1
        }
    });
    wrapper.vm.handlClick(3)
    // 发送事件
    expect(wrapper.emitted().input).toBeTruthy();
    // 发送事件的参数, 注意,是数组的形式
    // 这个事件发送的第一个参数[0]
    expect(wrapper.emitted().input[0]).toEqual([3])
  });
});

写到这里大家对测试也应该有了很多自己的想法, 没试过的小伙伴不妨试一试.

配置

上面没有提: 开启实时检测

"test:unit": "vue-cli-service test:unit --watch",
// 不管改没改, 所有文件都监控
"test:unit": "vue-cli-service test:unit --watchAll",

end

一套ui组件不写测试也是说不过去的, 写的过程也遇到很多很多的坑, 比如说两个相互以插槽嵌套的组件, 两个又都有'必传参数'的限制, vue没有很好的解决这个问题, 文档看了好久, 跟我的感觉就是有用的东西太少, 没办法这就是现状, 希望测试相关技术支持越来越完善吧.

大家都可以一起交流, 共同学习,共同进步, 早日实现自我价值!!

项目github地址: 链接描述
个人技术博客(ui官网):链接描述


lulu_up
5.7k 声望6.9k 粉丝

自信自律, 终身学习, 创业者