一、背景:
项目采用第三方框架uni-app开发微信小程序,vue语法 + wxApi
二、单测方案:
Vue Test Utils + JEST
前期调研:
综上:目前来说,方案一二官方提供的API不是很符合我们的需求,方案三相对来说更合适一些。
三、单测实施
1、测什么
单元测试(unit testing),是指对软件中的最小可测试单元进行检查和验证。
这个问题有很多博客写过答案(大部分来自与https://geosoft.no/unittesting.html 这个的翻译)。对于前端来说,我们的测试最小单元,也就是方法,组件。
2、FIRST原则
FIRST原则 | |
---|---|
F-FAST(快速原则) | 单元测试应该是可以快速运行的,在各种测试方法中,单元测试的运行速度是最快的,通常应该在几分钟内运行完毕 |
I-Independent(独立原则) | 单元测试应该是可以独立运行的,单元测试用例互相无强依赖,无对外部资源的强依赖 |
R-Repeatable(可重复原则) | 单元测试应该可以稳定重复的运行,并且每次运行的结果都是相同的 |
S-Self Validating(自我验证原则) | 单元测试应该是用例自动进行验证的,不能依赖人工验证 |
T-Timely(及时原则) | 单元测试必须及时的进行编写,更新和维护,以保证用例可以随着业务代码的变化动态的保障质量 |
3、结果检验-指标含义
coverage | |
---|---|
%stmts是语句覆盖率(statement coverage): | 是不是每个语句都执行了? |
%Branch分支覆盖率(branch coverage): | 是不是每个if代码块都执行了? |
%Funcs函数覆盖率(function coverage): | 是不是每个函数都调用了? |
%Lines行覆盖率(line coverage): | 是不是每一行都执行了? |
3、实际操作:
根据官网提示傻瓜式安装就好了
简单使用:
import { mount } from '@vue/test-utils'
// 你要测试的组件
import Counter from './counter'
// 渲染配置, 比如,你期望的data 初始值,props, store, slot,等
const config = {}
// 现在挂载组件,你便得到了这个包裹器, 就是你渲染的那个组件啦
const wrapper = mount(Counter, config)
// 你可以通过 `wrapper.vm` 访问实际的 Vue 实例
const vm = wrapper.vm //(比如可以调用实例的方法)
说明: wrapper是包裹器,vue test utils提供了对它操作的很多方法,比如.html(),.text (), .findComponent()等,用来获取内容,获取某个元素等,方便做断言判断,现在有很多即将废弃的方法,尽量避免使用。
config: 渲染组件的配置,可任意搭配
const store = new Vuex.Store({
state: {},
actions: {
aaa: jest.fn()
}
})
config = {
propsData: {
ddd: '1234'
},
filters: { bbb: jest.fn() },
store,
stubs,
localVue,
listeners: {
ccc: jest.fn()
}
}
常用方法: 比如这些:
import { mount, shallowMount, createLocalVue } from '@vue/test-utils'
const config = {}
wrapper = shallowMount(com, config)
wrapper.attributes()
wrapper.classes()
wrapper.findComponent('div')
wrapper.exists()
wrapper.setData({ aaa: 123 })
wrapper.setProps({ bbb: 123 })
...
tips: 我在项目中用到最多的是findComponent(class选择器)用户获取当前测试组件中的某个元素和子组件。这里有个小问题,findAllComponents 方法是用来查全部的,感觉类似queryselector 和 queryselectorAll。但是,findAllComponents查找的时候,如果查找的是多个子组件,那么返回的wrapperArray是有length的,调用正常,同api一致,如果查找div元素,返回值永远都是类似这种{selector: ‘.class’},无法获取length!!!!
jest.config.js: 有很多配置,我目前只配了关键用到的
module.exports = {
testEnvironment: 'jsdom', // 运行环境
moduleFileExtensions: [
'js',
'json',
'vue'
],
setupFiles: [
'/src/unitTest/setup.js' // 执行单测前执行的文件
],
transform: {
'.*\\.(vue)$': 'vue-jest',
'^.+\\.js?$': 'babel-jest'
},
moduleNameMapper: {
'^@/(.*)$': '/src/$1' // 解析@目录
},
testTimeout: 15000,
reporters: [
'default'
],
watchPathIgnorePatterns: ['/node_modules/', '/dist/', '/.git/'],
rootDir: __dirname,
testMatch: ['/src/**/unitTest/components/*test.[jt]s?(x)' ] // 测试文件目录
}
setup.js:
执行单测前会执行的文件,比如你定义的一些值。由于我的项目其实用了wxAPI嘛,vue是不识别的。对于单测来说,只需要测试某个方法是否执行,组件渲染是否与预期一直,存在边界值的时候是否兼容等,我们会mock很多方法。对于一些wxAPI的方法,项目中直接执行时是会报错的,jsdom环境不识别。所以,这些我们就可以在setup.js中去处理。
比如,我们在mounted里用到了wx.createSelectorQuery,单测时组件执行渲染就会报错,这个时候就可以在这里处理啦。当然啦,在当前组件的单测文件中处理也是可以的。
global.EVENTS = [
'touchstart',
'touchmove',
'touchcancel',
'touchend',
'click', // tap=>click
'longpress',
'longtap',
'transitionend',
'animationstart',
'animationiteration',
'animationend',
'touchforcechange',
'getphonenumber'
]
global.COMPONENTS = []
const configFun = {
getStorageSync () {},
reportAnalytics () {},
showToast () {}
}
configFun.createSelectorQuery = () => {
return {
in: () => {
return {
select: (selector) => {
return {
boundingClientRect (fn) {
return {
exec: function () {
fn({ width: 375 })
}
}
}
}
}
}
}
}
}
global.uni = global.wx = configFun
mock数据,mock函数:
对于组件中的函数,业务逻辑的话,完全可以用jest.fn()来代替。当我们单测只需要测试组件事件触发是否正确的调用函数,并不关心函数的执行结果时,可以用mock的函数代替原函数
比如:
(1)我们期望按钮点击触发onClick函数,可以这样写
const spyFn = jest.spyOn(wrapper.vm, 'onClick')
wrapper.findComponent('.btn').trigger('click')
expect(spyFn).toHaveBeenCalled()
(2)我们期望按钮点击触发$emit函数,可以这样写
const submitBtnClick = jest.fn()
beforeAll(() => {
wrapper = mount(com, {
listeners: {
submitBtnClick: submitBtnClick // 组件中点击时会$emit('submitBtnClick')
}
})
})
wrapper.findComponent('.r-item__head').trigger('click')
expect(submitBtnClick).toHaveBeenCalled()