乐趣区

第十二集-从零开始实现一套pc端vue的ui组件库-jest单元测试

第十二集: 从零开始实现(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 官网): 链接描述

退出移动版