lsekfe 发表于 2023-6-14 11:28:52

浅谈vue单元测试最佳实践(下)

四、更复杂的场景单元测试
  以某项目资产与发票的管理组件为场景,该组件是业务场景比较复杂的案例,交互如下:
http://www.51testing.com/attachments/2023/06/15326880_2023060915074914gg4.jpg
  为了完成组件的单元测试,牵涉到llsweb组件注册、过滤器注册、指令注册、原型改造、表格组件改造以及如何使用接口mock等。
  llsweb组件改造
  由于llsweb的llsBuryingPoint(“base”)是引入就执行的,里面有document.domain的逻辑,会报错,我暂时先把它注释了,后面可以改造成引入之后主动调用。
http://www.51testing.com/attachments/2023/06/15326880_202306091507521exL6.jpg
  然后本地build,本地引入。
http://www.51testing.com/attachments/2023/06/15326880_202306091507561XDqN.jpg
  过滤器改造
  原来的过滤器是默认使用Vue注册的,因此注册之后的过滤器会挂载到全局的Vue原型上。
http://www.51testing.com/attachments/2023/06/15326880_202306091507591kNbe.jpg
  而单元测试的代码是通过一个模拟器来执行的,会生成一个临时的localVue,因此应该把过滤器挂载到localVue上。
  import { shallowMount, mount,createLocalVue } from '@vue/test-utils';
  const localVue = createLocalVue()
  import filterV2 from '../../common/filterV2.js'
  filterV2(localVue);


  改造之后的过滤器如下:
http://www.51testing.com/attachments/2023/06/15326880_202306091508021cLjm.jpg
  全局指令与原型的改造
  改造方式如过滤器,改造之后如下
  import { shallowMount, mount,createLocalVue } from '@vue/test-utils';
  const localVue = createLocalVue()
  localVue.use(VueRouter)
  const router = new VueRouter();
  import VueRouter from 'vue-router'
  import llsweb from './lib/llsweb.min.js'
  //自定义公共过滤器,指令,方法和插件
  import filterV2 from '../../common/filterV2.js'
  import directiveV2 from'../../common/directiveV2.js'
  import prototypeV2 from'../../common/prototypeV2.js'
  directiveV2(localVue);
  filterV2(localVue);
  prototypeV2(localVue);
  localVue.use(llsweb)
  import tableV2 from '@/components/lls-tableV2'
  import columnV2 from'@/components/lls-table-columnV2'
  tableV2(localVue);
  columnV2(localVue);


  props设置
  wrapper = mount(usedAssetList, {
              propsData: {
                  isEdit: true,
                  detailData: {
                  },
                  accountsForm: {
                  },
                  assetNo: '',
                  assetNoArr: ['1'],
                  traceTableList: [],
                  appNo: ''
              },


  http请求的mock
  'vue-resource’的http请求mock方式如下:
  wrapper = mount(usedAssetList, {
              mocks: {
                  $http: {
                    get: function (url) {
                          if (!url) {
                              return Promise.resolve({ data: 'Mock Welcome!' });
                          }
                    },
                    post: function (url) {
                          if (url == '/common/debt/listDebtPage') {
                              return Promise.resolve({body:{"code":"200","message":"成功","url":"/spyPc-web/common/debt/listDebtPage","traceId":"1534069248765997058","data":{"records":[{"id":"1457","orgId":"0000000001","loanStatus":"{\"dictParam\":\"WAIT\",\"displayName\":\"待提交\",\"dictKey\":\"wait\"}","billType":"{\"dictParam\":\"INVOICE\",\"displayName\":\"发票\",\"dictKey\":\"invoice\"}","billNo":"44444444","billCheckSixCode":"443434","billAmount":4.00,"nonTaxAmount":4.00,"billDate":"2022-05-05","transferAmount":4.00,"checkStatus":"{\"dictParam\":\"WAIT_CHECK\",\"displayName\":\"待验真\",\"dictKey\":\"IVCS0101\"}","screenShotStatus":"{\"dictParam\":\"WAITING_CHECK\",\"displayName\":\"待查验\",\"dictKey\":\"ICSE100\"}","sellerId":"1483260622452768770","sellerName":"118供应商","buyerId":"1483276406675685377","buyerName":"118核心企业","billCode":"4444343443","enable":"{\"dictParam\":\"Y\",\"displayName\":\"启用\",\"dictKey\":1}","createBy":"460","createTime":"2022-05-26 13:35:22","updateBy":"0","updateTime":"2022-05-26 13:35:21","invoiceType":"{\"dictParam\":\"NORMAL\",\"displayName\":\"普票\",\"dictKey\":\"normal\"}","verifyCode":"443434","currency":"CNY","unpayAmount":0.00,"transferredAmount":4.00,"billValidAmount":4.00,"assetNo":"FC-2022-05-1620","baseContNo":"no89787899","baseContName":"合同034","onceTransferAmount":4.00,"onlyOnceRelationFlag":true}],"total":"1","size":"10","current":"1","queryCondition":{"onlyOnceRelationFlag":true,"assetNos":["FC-2022-05-1620","FC-2022-05-1621"]},"pages":"1","searchCount":true,"hitCount":false}}});
                          }
                    }
                  }
              },


  如果使用了axio的话:
  jest.mock("axios", () => ({
  get: (url) => {
      if(url == 'http://jsonplaceholder.typicode.com/posts') {
        return Promise.resolve({ data: [
        {
            "userId": 1,
            "id": 1,
            "title": "sunt aut facere repellat provident occaecati excepturi optio reprehenderit",
            "body": "abc"
        },
        {
            "userId": 1,
            "id": 2,
            "title": "qui est esse",
            "body": "xyz"
        }
        ] })
      }

  }
  }));


  官方提供的单元测试方案,对复杂的场景可能需要改造组件,过程对组件的侵入性很强,也相对复杂,因此很难应于实际业务中,那有没有简单通用的方案吗?
  五、单元测试最佳实践
  在某项目上实践过Vue官方推荐的单元测试方案一段时间之后,Vue-test-utils在使用过程中问题很多,对于复杂组件的测试更加困难,甚至没法测试。
  当把基于Vue-Test-Utils的单测方案从某应用到低代码业务组件库时,遇到了一个问题很难解决,在google上也没找到解决方法,浪费了大量时间和精力。最后决定放弃这种方案。
  那有没有更好的方案呢?
  (1)、基于Karma+mocha+chain的解决方案
  考虑到Element-ui这个应用广泛的库,肯定也有单元测试,于是决定借鉴他们的方案,读了Element-ui的源码中单元测试方案,他们使用的是基于Karma+mocha+chain的解决方案,基本思想是注册所有组件、用Vue创建实例,通过操作DOM的方法来实现的.
  注册所有组件、用Vue创建实例部分代码如下:
  import Vue from 'vue';
  import Element from 'main/index.js';
  Vue.use(Element);
  let id = 0;
  const createElm = function() {
  const elm = document.createElement('div');
  elm.id = 'app' + ++id;
  document.body.appendChild(elm);
  return elm;
  };
  /**
   * 回收 vm
   * @param{Object} vm
   */
  export const destroyVM = function(vm) {
  vm.$destroy && vm.$destroy();
  vm.$el &&
  vm.$el.parentNode &&
  vm.$el.parentNode.removeChild(vm.$el);
  };
  /**
   * 创建一个 Vue 的实例对象
   * @param{Object|String}Compo   组件配置,可直接传 template
   * @param{Boolean=false} mounted 是否添加到 DOM 上
   * @return {Object} vm
   */
  export const createVue = function(Compo, mounted = false) {
  if (Object.prototype.toString.call(Compo) === '') {
      Compo = { template: Compo };
  }
  return new Vue(Compo).$mount(mounted === false ? null : createElm());
  };
  /**
   * 创建一个测试组件实例
   * @link http://vuejs.org/guide/unit-testing.html#Writing-Testable-Components
   * @param{Object}Compo          - 组件对象
   * @param{Object}propsData      - props 数据
   * @param{Boolean=false} mounted- 是否添加到 DOM 上
   * @return {Object} vm
   */
  export const createTest = function(Compo, propsData = {}, mounted = false) {
  if (propsData === true || propsData === false) {
      mounted = propsData;
      propsData = {};
  }
  const elm = createElm();
  const Ctor = Vue.extend(Compo);
  return new Ctor({ propsData }).$mount(mounted === false ? null : elm);
  };
  /**
   * 触发一个事件
   * mouseenter, mouseleave, mouseover, keyup, change, click 等
   * @param{Element} elm
   * @param{String} name
   * @param{*} opts
   */
  export const triggerEvent = function(elm, name, ...opts) {
  let eventName;
  if (/^mouse|click/.test(name)) {
      eventName = 'MouseEvents';
  } else if (/^key/.test(name)) {
      eventName = 'KeyboardEvent';
  } else {
      eventName = 'HTMLEvents';
  }
  const evt = document.createEvent(eventName);
  evt.initEvent(name, ...opts);
  elm.dispatchEvent
      ? elm.dispatchEvent(evt)
      : elm.fireEvent('on' + name, evt);
  return elm;
  };
  /**
   * 触发 “mouseup” 和 “mousedown” 事件
   * @param {Element} elm
   * @param {*} opts
   */
  export const triggerClick = function(elm, ...opts) {
  triggerEvent(elm, 'mousedown', ...opts);
  triggerEvent(elm, 'mouseup', ...opts);
  return elm;
  };
  /**
   * 触发 keydown 事件
   * @param {Element} elm
   * @param {keyCode} int
   */
  export const triggerKeyDown = function(el, keyCode) {
  const evt = document.createEvent('Events');
  evt.initEvent('keydown', true, true);
  evt.keyCode = keyCode;
  el.dispatchEvent(evt);
  };
  /**
   * 等待 ms 毫秒,返回 Promise
   * @param {Number} ms
   */
  export const wait = function(ms = 50) {
  return new Promise(resolve => setTimeout(() => resolve(), ms));
  };
  /**
   * 等待一个 Tick,代替 Vue.nextTick,返回 Promise
   */
  export const waitImmediate = () => wait(0);


  这种方案的好处是方案成熟,与非MVVM的前端单测方案区别不大,但是缺点是写测试用例复杂,需要熟悉原生JS方法、且代码量比较大、需要自己股管理回收Vue实例、实现事件等。
  (2)、基于Karma+mocha+Vue-test-utils+chain的解决方案
  其实??Vue-test-utils???给我们提供了操作Vue组件的方法,借助它可以减少手动操作原生DOM的场景、不用自己去管理Vue实例和事件、且天然支持组件props通信、支持??浅渲染??、更优雅的实现异步以及Mock、Vue-router等。
  因此基于Karma+mocha+chain的解决方案加上Vue-test-utils就形成了现有的单元测试方案--基于Karma+mocha+Vue-test-utils+chain的解决方案,并且支持两种写法。
  示例如下:
  import Vue from 'vue'
  import taskTodoChart from '@/components/taskTodoChart/index.vue'
  import { createTest, createVue, destroyVM } from '../util';
  //引入vue-test-utils
  import { mount } from 'vue-test-utils'
  describe('vue-test-utils测试taskTodoChart组件', () => {
      it('待领取的数量应该等于1,待办理数量应该等于23', async () => {
        const wrapper =mount(taskTodoChart, {
              propsData: {
              parent: {
                  unAssignedData:1,
                  assignedData:23,
                  router:"/#/ewewewew"
              }
              }
        });
        await Vue.nextTick()
        const unAssignedData =wrapper.find('.unAssignedData');
        const assignedData =wrapper.find('.assignedData');
        expect(unAssignedData.text()).to.equal('待领取:1')
        expect(assignedData.text()).to.equal('待办理:23')
      })
  })
  describe('Mocha测试taskTodoChart组件', () => {
      it('待领取的数量应该等于1,待办理数量应该等于23', async () => {
        const vm = createTest(taskTodoChart, {
              parent: {
                  unAssignedData:1,
                  assignedData:23,
                  router:"/#/ewewewew"
              }
        }, true);
        await Vue.nextTick()
        expect((vm.$el.querySelector('.unAssignedData').textContent)).to.equal('待领取:1');
        expect((vm.$el.querySelector('.assignedData').textContent)).to.equal('待办理:23');
      })
  })


  六、总结
  基于Karma+mocha+Vue-test-utils+chain的解决方案是在Karma+mocha+chain的方案之上引入了Vue-test-utils工具库并保留了Element-ui的原有单元测试方案,它更适用于Vue的单元测试且能用于任何非Vue的项目。

页: [1]
查看完整版本: 浅谈vue单元测试最佳实践(下)