TA的每日心情 | 擦汗 昨天 09:04 |
---|
签到天数: 1047 天 连续签到: 5 天 [LV.10]测试总司令
|
四、更复杂的场景单元测试
以某项目资产与发票的管理组件为场景,该组件是业务场景比较复杂的案例,交互如下:
为了完成组件的单元测试,牵涉到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的项目。
|
|