51Testing软件测试论坛

 找回密码
 (注-册)加入51Testing

QQ登录

只需一步,快速开始

微信登录,快人一步

查看: 542|回复: 0
打印 上一主题 下一主题

[资料] 浅谈vue单元测试最佳实践(下)

[复制链接]
  • TA的每日心情
    无聊
    昨天 11:40
  • 签到天数: 943 天

    连续签到: 2 天

    [LV.10]测试总司令

    跳转到指定楼层
    1#
    发表于 2023-6-14 11:28:52 | 只看该作者 回帖奖励 |倒序浏览 |阅读模式
    四、更复杂的场景单元测试
      以某项目资产与发票的管理组件为场景,该组件是业务场景比较复杂的案例,交互如下:

      为了完成组件的单元测试,牵涉到llsweb组件注册、过滤器注册、指令注册、原型改造、表格组件改造以及如何使用接口mock等。
      llsweb组件改造
      由于llsweb的llsBuryingPoint(“base”)是引入就执行的,里面有document.domain的逻辑,会报错,我暂时先把它注释了,后面可以改造成引入之后主动调用。

      然后本地build,本地引入。

      过滤器改造
      原来的过滤器是默认使用Vue注册的,因此注册之后的过滤器会挂载到全局的Vue原型上。

      而单元测试的代码是通过一个模拟器来执行的,会生成一个临时的localVue,因此应该把过滤器挂载到localVue上。
      import { shallowMount, mount,  createLocalVue } from '@vue/test-utils';
      const localVue = createLocalVue()
      import filterV2 from '../../common/filterV2.js'
      filterV2(localVue);


      改造之后的过滤器如下:

      全局指令与原型的改造
      改造方式如过滤器,改造之后如下
      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) === '[object String]') {
          Compo = { template: Compo };
        }
        return new Vue(Compo).$mount(mounted === false ? null : createElm());
      };
      /**
       * 创建一个测试组件实例
       * @link http://vuejs.org/guide/unit-test ... 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的项目。

    分享到:  QQ好友和群QQ好友和群 QQ空间QQ空间 腾讯微博腾讯微博 腾讯朋友腾讯朋友
    收藏收藏
    回复

    使用道具 举报

    本版积分规则

    关闭

    站长推荐上一条 /1 下一条

    小黑屋|手机版|Archiver|51Testing软件测试网 ( 沪ICP备05003035号 关于我们

    GMT+8, 2024-5-8 03:25 , Processed in 0.070097 second(s), 23 queries .

    Powered by Discuz! X3.2

    © 2001-2024 Comsenz Inc.

    快速回复 返回顶部 返回列表