最近Vue项目要求用到单元测试,就这段时间用到的一些单元测试知识做一下总结。
TDD和BDD的概念
1.TDD(Test Driven Development),即测试驱动开发,简单的来说就是先编写测试代码,然后以使得所有测试代码都通过为目的,编写逻辑代码,是一种以测试来驱动开发过程的开发模式。
2.BDD(Behavior Driven Development),即行为驱动开发,简单的来说就是先编写业务逻辑代码,然后以使得所有业务逻辑按照预期结果执行为目的,编写测试代码,是一种以用户行为来驱动开发过程的开发模式。
3.集成测试(Integration Testing),是指对软件中的所有模块按照设计要求进行组装为完整系统后,进行检查和验证。通俗的讲,在前端,集成测试可以理解为对多个模块实现的一个交互完整的交互流程进行测试。对于多个模块(ES6模块)组成的系统,需要首先将交互行为完善,才能按照预期行为编写测试代码。所以提到BDD,这里的测试一般是指集成测试。
单元测试要测什么
测试的目的:Vue官网《明白要测试的是什么》这一节
测试方向简单来说就几点:
①测试函数是否被调用(自定义事件$emit,method中的方法)
②props、样式是否生效
③watch监听是否生效
④DOM结构是否存在
环境配置
1.jest
在原有vue-cl安装了单元测试i基础上,可以直接添加jest,添加后会在目录下创建jest.config.js用于配置和预设。
vue add @vue/unit-jest
其中配置项主要有这几点(我的项目为例)
module.exports = {
preset: '@vue/cli-plugin-unit-jest',
moduleNameMapper: {
Swiper: '<rootDir>/tests/unit/__mocks__/Swiper.js', // 在mocks文件夹创建假module.exports 导出第三方框架
echarts: '<rootDir>/tests/unit/__mocks__/echarts.js',
BaseUI: '<rootDir>/tests/unit/__mocks__/baseui.js',
'^@\/(.*?\.?(js|vue)?|)$': '<rootDir>/src/$1', // @路径转换,例如:@/components/Main.vue -> rootDir/src/components/Main.vue
'\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$': '<rootDir>/tests/unit/__mocks__/fileMock.js', // 模拟加载静态文件
'\\.(css|less|scss|sass)$': '<rootDir>/tests/unit/__mocks__/styleMock.js' // 模拟加载样式文件
},
setupFilesAfterEnv: ['<rootDir>/tests/unit/__mocks__/setup-tests.js'],
moduleFileExtensions: [
'js',
'json',
// 告诉 Jest 处理 `*.vue` 文件
'vue'
],
transform: {
// 用 `vue-jest` 处理 `*.vue` 文件
'.*\\.(vue)$': 'vue-jest',
// 用 `babel-jest` 处理 js
'^.+\\.js$': '<rootDir>/node_modules/babel-jest'
},
collectCoverage: true,
collectCoverageFrom: [
'!**/src/components/editable-tabs/editable-tabs.vue**',
'!**/src/components/HelloWorld.vue**',
'!**/src/components/all-modal/menu-modal/menu-modal.vue**',
'!**/src/basic-comps/base-echarts/base-echarts.vue**',
'!**/src/basic-comps/basic-swiper/basic-swiper.vue**',
'!**/src/basic-comps/sys-menu/sys-menu.vue**',
// '**/*.{vue,js,jsx}',
'**/src/basic-comps/**/*.{vue,jsx}',
'**/src/components/**/*.{vue,jsx}',
// '**/src/utils/*.{js,jsx}',
'!**/node_modules/**',
'!**/doc/**',
'!**/vendor/**',
'!**/dist/**'
]
}
testMatch - 匹配测试用例的文件
transform - 用 vue-jest 处理 *.vue 文件,用babel-jest 处理 *.js 文件
moduleNameMapper - 支持源代码中相同的 @ -> src 别名
coverageDirectory - 覆盖率报告的目录,测试报告所存放的位置
collectCoverageFrom - 测试报告想要覆盖那些文件,目录,前面加!是避开这些文件
2.Vue-Test-Utils
建议查看官网api和教程:https://vue-test-utils.vuejs.org/zh/guides/
测试demo
项目大佬写的demo,开头参考文章是了解各种语法和函数、钩子的作用的资料
/**
* 参考文章:
* https://vue-test-utils.vuejs.org/zh/api/wrapper/
* https://www.jianshu.com/p/ad87eaf54622
* https://juejin.im/post/6856730547969622024#heading-5
* https://juejin.im/post/6844904196244766728#heading-37
* https://juejin.im/post/6844904051449036808#heading-11
*/
import { createLocalVue, shallowMount } from '@vue/test-utils'
import ElementUI from 'element-ui'
import Demo from '@/components/demo/demo.vue'
const localVue = createLocalVue()
localVue.use(ElementUI)
describe('Demo.vue', () => {
let wrapper = null
const msg = 'new message'
const mocks = {
$router: {
push: jest.fn()
}
}
beforeEach(() => {
// shallowMount创建的组件,只关注当前的组件,不应该关注其子组件的情况
wrapper = shallowMount(Demo, {
localVue,
propsData: { msg },
slots: {
default: '<section class="content">What an awesome section</section>'
},
mocks: mocks // 将全局属性存根, 然后就可以通过 wrapper.vm.$router 访问 push
})
})
afterEach(() => {
wrapper.destroy()
})
describe('1.测试渲染', () => {
it('renders props.msg when passed', () => {
expect(wrapper.text()).toMatch(msg)
})
})
describe('2.测试渲染之后生成快照', () => {
it('测试渲染之后生成快照', () => {
expect(wrapper.vm.$el).toMatchSnapshot() // 会生把渲染之后的结果,生成一个文件
})
})
describe('3.测试DOM结构', () => {
// exists():断言 Wrapper 或 WrapperArray 是否存在。
test('不存在img', () => {
expect(wrapper.findAll('img').exists()) // https://vue-test-utils.vuejs.org/zh/api/wrapper/#exists
.toBeFalsy()
})
// 断言 Wrapper 包含 TestMount 子组件。
it('TestDemo组件不为空', () => {
// https://vue-test-utils.vuejs.org/zh/api/wrapper/#findcomponent
const testMount = wrapper.findComponent({ name: 'test' }) // => finds TestDemo by component instance
expect(testMount.exists())
.toBe(true)
})
// attributes():返回 Wrapper DOM 节点的特性对象
// classes():返回 Wrapper DOM 节点的 class 组成的数组
// it('TestDemo组件有 test-demo 类', () => {
// console.log(wrapper.find('.test-demo'))
// console.log(wrapper.find('.test-demo').attributes()) // { class: 'test-demo btn', style: 'width: 100%;' }
// console.log(wrapper.find('.test-demo').classes()) // [ 'test-demo', 'btn' ]
expect(wrapper.find('.test-demo').attributes().class) // // https://vue-test-utils.vuejs.org/zh/api/wrapper/#attributes
.toContain('btn')
// expect(wrapper.find('.test-demo').classes()) // https://vue-test-utils.vuejs.org/zh/api/wrapper/#classes
// .toContain('test-demo')
// })
})
describe('4.测试样式', () => {
test('测试样式', () => {
expect(wrapper.find('.test-demo').attributes().style)
.toContain('width: 100%')
})
})
describe('5.测试Props', () => {
test('测试Props', () => {
wrapper.setProps({ msg: 'test-demo' })
expect(wrapper.props().msg)
.toBe('test-demo')
})
})
describe('6.测试自定义事件evnet', () => {
// 使用监听的方法,不会提示过期
test('测试自定义事件evnet', () => {
// 创建mock函数
const spyHandleTitleClick = jest.spyOn(wrapper.vm, 'handleTitleClick')
// const spyAxiosRequest = jest.spyOn(axios, 'get');
// 触发按钮的点击事件
wrapper.find('.title').trigger('click')
// 判断该方法是否已经被调用
expect(spyHandleTitleClick).toHaveBeenCalled()
// 获取判断数据是否发生改变
// expect(spyAxiosRequest).toBeCalledWith(axiosRequestURL, axiosRequestParams);
})
// 测试是否触发了emit函数
test('测试自定义事件evnet', () => {
const spyEmit = jest.spyOn(wrapper.vm, '$emit')
wrapper.find('.title').trigger('click')
expect(spyEmit).toHaveBeenCalled()
expect(spyEmit).toHaveBeenCalledTimes(2)
})
})
describe('7.测试计算属性computed', () => {
test('1.测试formatNunber', () => {
wrapper.setData({ number: 10 })
expect(wrapper.vm.formatNumber).toBe('数值=10') // formatNumber 是计算属性
})
test('2.测试formatNunber', () => {
wrapper.find('.btn-number').trigger('click')
expect(wrapper.vm.formatNumber).toBe('数值=2')
})
})
describe('8.测试监听器watch', () => {
test('测试watch number', (done) => {
const spy = jest.spyOn(console, 'log')
wrapper.vm.number = 20 // change number
// watch number
// watch中的方法被Vue**推迟**到了更新的下一个循环队列中去异步执行,如果这个watch被触发多次,只会被推送到队列一次。
// 这种缓冲行为可以有效的去掉重复数据造成的不必要的性能开销。
// 所以当我们设置了inputValue为'ok'之后,watch中的方法并没有立刻执行
wrapper.vm.$nextTick(() => {
expect(spy)
.toBeCalled()
// 清除数据
spy.mockClear()
done()
})
})
})
// jest.mock('axios')
// 在测试时要避免一切的依赖,将所有的依赖都mock掉
describe('9.测试方法method', () => {
test('测试 handleTitleClick 方法methcd,推荐', () => {
// 创建mock函数
const spyHandleTitleClick = jest.spyOn(wrapper.vm, 'handleTitleClick')
// 触发按钮的点击事件
wrapper.find('.title').trigger('click')
// 判断该方法是否已经被调用
expect(spyHandleTitleClick)
.toHaveBeenCalled()
})
})
// https://vue-test-utils.vuejs.org/zh/api/wrapper/#findall
describe('10.测试插槽slot(其实就是测试dom)', () => {
test('测试header插槽slot', () => {
const header = wrapper.find('header') // 查找header标签
expect(header.text()).toBe('What an awesome header')
})
test('测试default插槽slot', () => {
expect(wrapper.findAll('.content').exists())
.toBeTruthy()
})
test('测试footer插槽slot', () => {
expect(wrapper.find('footer').exists())
.toBeTruthy()
})
})
})