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

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

1.聊聊测试

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

2.优缺点

缺点vue

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

优势node

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

3.用法与分类

大致上分为两类:git

  1. BDD 把全部逻辑都写好, 而后根据你的总体逻辑制定你的测试, 好处固然是好理解,更有总体思惟, 缺点就是覆盖率低, 并非很保险.
  2. TDD 把测试写好再进行开发, 这个模式挺有意思, 先写测试, 也就是在脑中先总体布局, 每一步都是本身思考好了再去作的测试覆盖率多是100%, 他的缺点就太明显了, 开发人员技术必须硬, 并且若是改需求...有的忙了.

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

"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.jsvue-cli

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

以偶上述为例npm

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

多种类型的'配置器'element-ui

  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官网):连接描述

相关文章
相关标签/搜索