1.安装
安装方式1
- 如果正常执行的话,修改 .eslintrc.js,package.json(增加test:unit命令),增加tests文件夹(units/example.spec.js)
- 经常安装不成功,导致执行出错,提示render不存在等报错,那就采用第二种方式安装
vue add unit-jest
安装方式2
- 把下面的内容粘贴到package.json,执行yarn
"@vue/cli-plugin-unit-jest": "^4.5.17",
"@vue/server-test-utils": "^1.3.0",
"@vue/test-utils": "^1.0.3",
"vue-server-renderer": "2.6.12",
- 修改.eslintrc.js,增加overrides,与rules同级,其实增加单元测试eslint代码测试
overrides: [
{
files: [
'**/__tests__/*.{j,t}s?(x)',
'**/tests/unit/**/*.spec.{j,t}s?(x)'
],
env: {
jest: true
}
}
]
- 这里更推荐在测试组件所在目录建__tests__,然后新建xxx.spec.js
如测试Hello.vue,我们就在同级目录创建__tests__,然后创建Hello.spec.js,有时候为了方法查找,执行单个单元测试,名字需唯一,则加入父文件夹名称,如parent_Hello.spec.js,yarn test:unit parent_Hello.spec.js,就可以执行单个单元测试了
2.测试用例,官方文档
- 断言jest,官方网站
- 组件挂载@vue/test-utils,官方网站
写测试用例
- 测试用例建议写在组件同级目录,就近维护,建__tests__文件夹存放相关测试用例
- jest.config.js 更多配置
- transformIgnorePatterns 必须配置,默认直接忽略node_modules的所有import,负责导致ant-design-vue等import报错
module.exports = {
preset: '@vue/cli-plugin-unit-jest',
collectCoverage: false,
collectCoverageFrom: [
'src/components/customizeFormItem/*.{vue,jsx}',
'src/components/table/.{vue,jsx}',
'src/components/tableBatch/.{vue,jsx}',
'src/components/.{vue,jsx}',
'src/views/tools/**/*.{vue,jsx}',
'src/views/user/**/*.{vue,jsx}',
'src/views/wx/**/*.{vue,jsx}',
'src/utils/*.js',
'src/store/*.js'
],
moduleNameMapper: {
'^@/(.*)$': '<rootDir>/src/$1',
'^ant-design-vue$': '<rootDir>/node_modules/ant-design-vue/es/index.js',
'^vue$': '<rootDir>/node_modules/vue/dist/vue.common.js',
'^vxe-table$': '<rootDir>/node_modules/vxe-table/lib/index.common.js'
},
transformIgnorePatterns: ['node_modules/(?!(ant-design-vue|vxe-table|vue|element-ui)/)'],
setupFiles: [
'<rootDir>/tests/setup.js'
]
}
- 测试用例:vue组件
import { mount } from '@vue/test-utils'
import InputNumber from '@/components/customizeFormItem/InputNumber'
import { InputNumber as AntInputNumber } from 'ant-design-vue'
describe('InputNumber.vue', () => {
let wrapper
beforeEach(() => {
wrapper = mount(InputNumber, {
listeners: {
change(val) {
wrapper.setProps({
value: val
})
}
},
attrs: {
addonBefore: '美元',
addonAfter: '$'
}
})
})
it('检测slot:addonBefore和addonAfter', async () => {
expect(wrapper.text()).toMatch('美元$')
expect(wrapper.findAllComponents(AntInputNumber)).toHaveLength(1)
})
it('模拟人工输入', async () => {
await wrapper.find('input').setValue('1000')
expect(wrapper.props().value).toBe(1000)
expect(wrapper.vm.localValue).toBe(1000)
})
afterEach(() => {
wrapper.destroy()
})
})
import { mount } from '@vue/test-utils'
import EcSelect from '@/components/customizeFormItem/EcSelect.vue'
const stubs = [
'vxe-list',
]
describe('EcSelect.vue', () => {
let wrapper
beforeEach(() => {
wrapper = mount(EcSelect, {
stubs,
propsData: {
options: [
{ label: '张三', value: '1' },
{ label: '李四', value: '2' },
{ label: '王五', value: '3' },
{ label: '赵六', value: '4' }
],
multiple: true,
value: ['1', '2']
},
data() {
return {
visible: true
}
},
listeners: {
input: function (val) {
wrapper.setProps({ value: val })
}
}
})
})
it('测试单选-默认值是否选中', async () => {
expect(wrapper.vm.selectLabel).toEqual(['张三', '李四'])
})
afterEach(() => {
wrapper.destroy()
})
})
- 函数测试用例:测试函数和vuex(mutations, acitons里面的方法)
import {
merge,
deepClone
} from '../utils'
it('merge', () => {
const obj1 = { a: 1, b: 1 }
const obj2 = { a: 2, children: [{ a: 1 }], c: 2 }
const obj3 = { a: 3, children: [{ a: 3 }, { b: 3 }], d: 3 }
const target = { a: 3, children: [{ a: 3 }, { b: 3 }], b: 1, c: 2, d: 3 }
expect(merge(obj1, obj2, obj3)).toEqual(target)
})
describe('deepClone', () => {
it('复制对象', () => {
const obj = { a: 1, b: 1 }
expect(deepClone(obj)).toEqual(obj)
})
it('复制数组', () => {
const arr = [1, 2, 3, { a: 1 }]
expect(deepClone(arr)).toEqual(arr)
})
})
3. 经验
- shallowMount和mount
- 两者如果没有直接在组件内部注册,执行单元测试,会提示组件没有注册
- 如果组件有注册过,shallowMount会把当前组件components没有覆盖的组件,自动转为xxx-stub,而mount会把所有的组件都渲染出来
- mount比shallowMount执行时间长,占内存多
- 正常shallowMount挂载,只有当你需要渲染整个组件的时候,采用mount,这也会导致你需要处理更多情况,比如子组件用了router,vuex等
- shallowMount和mount的配置项,更多配置项
wrapper = mount(EcSelect, {
context: { // 将上下文传递给函数式组件。该选项只能用于函数组件,比较少用
props: { show: true },
children: [Foo, Bar]
}
stubs, // 组件占位符,['a-tag', 'a-button'] 或者 { 'a-tag': Tag, 'a-button': true }
propsData: { // 等同于给组件传递props
options: [
{ label: '张三', value: '1' },
{ label: '李四', value: '2' },
{ label: '王五', value: '3' },
{ label: '赵六', value: '4' }
],
multiple: true,
value: ['1', '2']
},
slots: { 插入slots ,组件内使用$slots获取
baz: bazComponent, // 支持组件
qux: '<my-component />随便写两字', // 支持字符串组件,但是需在stubs配置{ 'my-component': myComponent }
default: [Foo, '<my-component />', 'text'] // 支持数组,字符串组件,但是需在stubs配置{ 'my-component': myComponent }
},
scopedSlots: { // 插入scopedSlots,组件内使用$scopedSlots获取
default: '<p>{{props.index}},{{props.text}}</p>', // 支持字符串
foo(props) { // 也支持函数
return <div>{props.text}</div>
}
},
mocks: { // 虚拟组件内的全局方法
$xxx: () => {}
},
data() {
return {
visible: true
}
},
listeners: {
input: function (val) {
wrapper.setProps({ value: val })
}
},
router,
store,
localVue,
provide() {
return {
$IndexCtx: {}
}
}
...其他选项
})
- 执行单个测试用例,yarn test:unit xxx.spec.js(全局目录搜索),或者 yarn test:unit xxx/xxx/xxx.spec.js(执行同目录的类似名字文件)),基本都能定位到当前组件,如果实在如法定位,就使用唯一命名
- 监听并执行测试用例:yarn test:unit test xxx.spec.js --watch // 默认执行 jest -o 监视有改动的测试
- yarn test:unit --coverage 生成测试报告,
- 哪些页面可以写用测用例
- 纯函数(utils/xxx.js)
- vuex的mutations,actions
- 非业务组件,业务组件需要调用接口,无法用模拟接口来测试接口
- beforeEach 和 afterEach,describe,expect,it, test... 是jest全局函数或者关键字,beforeEach 和 afterEach需在describe内部运行,三个函数都是非必须的
- 常用断言toBe(非引用类型, boolen, number, string) toMatch(字符串类型), toEqual(引用类型,array, object)
- mount配置项内的data函数,将会覆盖组件的data函数