# 1.创建项目
vue create vue-jest-demo # 新建一个vue的项目(选择jest单元测试)
# 2.选择自定义配置
default (babel, eslint)
> Manually select features
# 3.选择安装feature,也可以根据自己的喜好去安装
(*) Babel
( ) TypeScript
( ) Progressive Web App (PWA) Support
(*) Router
(*) Vuex
(*) CSS Pre-processors
(*) Linter / Formatter
>(*) Unit Testing
( ) E2E Testing
# 选择测试框架,我们选择jest
Mocha + Chai
>(*) Jest
安装完毕后,会在根目录下存在一个 tests 的目录,其中里面包含一个简单的测试用例 example.spec.js 文件 和 jest.config.js 单元测试配置文件
LiuJun-MacBook-Pro:vue-test-demo liujun$ tree
.
├── README.md
├── babel.config.js
├── jest.config.js # 单元测试的配置
├── package-lock.json
├── package.json # 项目的依赖
├── public
│ ├── favicon.ico
│ └── index.html
├── src
│ ├── App.vue
│ ├── assets
│ │ └── logo.png
│ ├── components
│ │ └── HelloWorld.vue
│ ├── main.js
│ ├── router
│ │ └── index.js
│ ├── store
│ │ └── index.js
│ └── views
│ ├── About.vue
│ └── Home.vue
└── tests # 单元测试的文件
└── unit
└── example.spec.js
项目的package.json文件
{
"name": "vue-test-demo",
"version": "0.1.0",
"private": true,
"scripts": {
"serve": "vue-cli-service serve",
"build": "vue-cli-service build",
"test:unit": "vue-cli-service test:unit",
"lint": "vue-cli-service lint"
},
"dependencies": {
"core-js": "^3.6.5",
"vue": "^2.6.11",
"vue-router": "^3.2.0",
"vuex": "^3.4.0"
},
"devDependencies": {
"@vue/cli-plugin-babel": "~4.5.0",
"@vue/cli-plugin-router": "~4.5.0",
"@vue/cli-plugin-vuex": "~4.5.0",
"@vue/cli-service": "~4.5.0",
"@vue/cli-plugin-unit-jest": "~4.5.0",
"@vue/test-utils": "^1.0.3",
"@vue/eslint-config-standard": "^5.1.2",
"@vue/cli-plugin-eslint": "~4.5.0",
"babel-eslint": "^10.1.0",
"eslint": "^6.7.2",
"eslint-plugin-import": "^2.20.2",
"eslint-plugin-node": "^11.1.0",
"eslint-plugin-promise": "^4.2.1",
"eslint-plugin-standard": "^4.0.0",
"eslint-plugin-vue": "^6.2.2",
"node-sass": "^4.12.0",
"sass-loader": "^8.0.2",
"vue-template-compiler": "^2.6.11"
}
}
项目 jest.config.json 文件的配置
module.exports = {
preset: '@vue/cli-plugin-unit-jest'
}
npm run test:unit
# 控制台输出的结果
LiuJun-MacBook-Pro:vue-test-demo liujun$ npm run test:unit
> vue-test-demo@0.1.0 test:unit /Users/liujun/Documents/huayun/test/vue-test-demo
> vue-cli-service test:unit
PASS tests/unit/example.spec.js
HelloWorld.vue
✓ renders props.msg when passed (40ms)
Test Suites: 1 passed, 1 total
Tests: 1 passed, 1 total
Snapshots: 0 total
Time: 5.595s
Ran all test suites.
module.exports = {
preset: '@vue/cli-plugin-unit-jest',
moduleNameMapper: {
"^@/(.*)$": "/src/$1"
},
testMatch: [
"**/tests/unit/**/*.spec.(js|jsx|ts|tsx)|**/__tests__/*.(js|jsx|ts|tsx)"
],
collectCoverage: true,
coverageDirectory: "/tests/unit/coverage" ,
collectCoverageFrom: [
"src/components/**/*.vue",
"src/utils/**/*.ts",
"src/store/modules/*.ts",
"!src/utils/axios.ts",
"!src/utils/notify.ts"
]
}
配置的解释如下:
preset: 预设的vue单元测试使用的插件。
moduleNameMapper:模块别名配置。
testMatch:测试文件查找规则,可以是统一放在src/tests目录下,也可以就近放在_ _tests__目录下。
collectCoverage:是否进行测试覆盖率收集。
coverageDirectory:测试报告存放位置。
collectCoverageFrom:测试哪些文件和不测试哪些文件,你可以根据你的团队或者个人偏好进行设置。
在完善以上配置后,在终端运行npm run test:unit命令,可看测试覆盖率和测试报告。
> vue-cli-service test:unit
PASS tests/unit/example.spec.js
HelloWorld.vue
✓ renders props.msg when passed (36ms)
----------|----------|----------|----------|----------|-------------------|
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s |
----------|----------|----------|----------|----------|-------------------|
All files | 0 | 0 | 0 | 0 | |
----------|----------|----------|----------|----------|-------------------|
Test Suites: 1 passed, 1 total
Tests: 1 passed, 1 total
Snapshots: 0 total
Time: 5.17s
Ran all test suites.
1.编写 HelloWorld.vue组件
<template>
<div class="hello">
<slot name="title">
<span>count:{{count}}span>
slot>
<button
class="btn btn-primary"
:style="{height:'50px'}"
@click="handlerBtnClick">
{{msg}}
button>
div>
template>
<script>
export default {
name: 'HelloWorld',
props: {
msg: String,
defaultCount: {
type: Number,
default: 0
}
},
data () {
return {
count: this.defaultCount
}
},
methods: {
handlerBtnClick () {
this.count = this.count + 1
}
}
}
script>
<style scoped lang="scss">
.btn {
color:green;
}
style>
2.编写 helloworld.spec.js 文件, 使用 ( @vue/test-utils库测试 )
import { shallowMount } from '@vue/test-utils'
import HelloWorld from '@/components/HelloWorld.vue'
describe('HelloWorld.vue', () => {
it('create', () => {
const msg = '按钮名称'
const wrapper = shallowMount(HelloWorld, {
propsData: { msg }
})
expect(wrapper.text()).toMatch(msg)
})
})
3.执行 npm run test:unit 开始测试
或者
编写 helloworld.spec.js 文件, 不使用 ( @vue/test-utils库测试 )
import { createTest, createVue, destroyVM } from './utils';
import HelloWorld from '@/components/HelloWorld.vue'
describe('HelloWorld', () => {
let vm;
afterEach(() => {
destroyVM(vm);
});
it('create', () => {
vm = createTest(HelloWorld, {
msg: '按钮名称'
}, true);
let buttonElm = vm.$el;
expect(buttonElm.querySelector('.btn-primary').innerHTML).toContain('按钮名称')
});
})
utils.js文件
// import Vue from 'vue';
import Vue from 'vue/dist/vue.js'; // 包含模板的编译模块
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-testing.html#Writing-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);
};
执行 npm run test:unit 开始测试
1.使用 ( @vue/test-utils库测试) 的代码
import { shallowMount } from '@vue/test-utils'
import HelloWorld from '@/components/HelloWorld.vue'
describe('HelloWorld.vue', () => {
it('create', () => {
const msg = '按钮名称'
const wrapper = shallowMount(HelloWorld, {
propsData: { msg }
})
expect(wrapper.text()).toMatch(msg)
})
it('msg', () => {
const msg = '按钮'
const wrapper = shallowMount(HelloWorld, {
propsData: { msg }
})
expect(wrapper.text()).toMatch(msg)
})
it('defaultCount', () => {
const defaultCount = 10
const wrapper = shallowMount(HelloWorld, {
propsData: { defaultCount }
})
expect(wrapper.find('span').text()).toBe("count:"+defaultCount)
})
it('click', (done) => {
const defaultCount = 0
const wrapper = shallowMount(HelloWorld, {
propsData: { defaultCount }
})
wrapper.find('.btn').trigger('click')
wrapper.vm.$nextTick(()=>{
// 界面跟新完成之后再断言
expect(wrapper.find('span').text()).toBe("count:"+(defaultCount+1))
done()
})
})
it('slot title', () => {
const titleHtml = ' liujun-title '
const wrapper = shallowMount(HelloWorld, {
slots: {
title: titleHtml
}
})
expect(wrapper.find('.liujun-title').html()).toContain(titleHtml)
})
})
2.不使用 ( @vue/test-utils库测试) 的代码
import { createTest, createVue, destroyVM } from './utils';
import HelloWorld from '@/components/HelloWorld.vue'
describe('HelloWorld', () => {
let vm;
afterEach(() => {
destroyVM(vm);
});
it('create', () => {
vm = createTest(HelloWorld, {
msg: '按钮名称'
}, true);
let buttonElm = vm.$el;
expect(buttonElm.querySelector('.btn-primary').innerHTML).toContain('按钮名称')
});
it('msg', () => {
vm = createTest(HelloWorld, {
msg: '按钮名称'
}, true);
let buttonElm = vm.$el;
expect(buttonElm.querySelector('.btn-primary').innerHTML).toContain('按钮名称')
});
it('defaultCount', () => {
let defaultCount = 10
vm = createTest(HelloWorld, {
defaultCount
}, true);
let buttonElm = vm.$el;
expect(buttonElm.querySelector('span').innerHTML).toBe('count:'+defaultCount)
});
it('click', (done) => {
let defaultCount = 10
// 创建一个vue实例,并挂在dom上
vm = createTest(HelloWorld, {
msg: '按钮名称',
defaultCount
}, true);
let buttonElm = vm.$el;
buttonElm.querySelector('.btn-primary').click()
setTimeout(_ => {
// 等界面刷新之后断言
expect(buttonElm.querySelector('span').innerHTML).toBe('count:'+(defaultCount+1))
done();
}, 20);
});
it('slot title', () => {
// 创建一个vue实例,并挂在dom上
vm = createVue({
template: `
liujun-title
HelloWorld 按钮
`,
components: {
HelloWorld
}
}, true);
let buttonElm = vm.$el;
expect(buttonElm.querySelector('.liujun-title').innerHTML).toContain('liujun-title')
});
})
1.项目安装element组件库
npm install element-ui -S
# main.js
import Vue from 'vue'
import Element from 'element-ui'
Vue.use(Element)
2.编写 Login.vue组件
提交
重置
3.编写 Login.spec.js ( 使用@vue/test-utils测试库 )
import { shallowMount, mount, createLocalVue} from '@vue/test-utils'
import Login from '@/components/Login.vue'
import ElementUI, {
Input,
Button
} from "element-ui"
const localVue = createLocalVue()
localVue.use(ElementUI)
describe('Login.vue', () => {
it('create', () => {
const wrapper = shallowMount(Login, {
localVue
})
expect(wrapper.findComponent(Button).text()).toMatch('提交')
})
it('age', () => {
const age = 100
const wrapper = shallowMount(Login, {
localVue,
propsData: { age }
})
expect(wrapper.findComponent(Input).vm.value).toBe(age)
})
it('success submit form', () => {
const age = 100
const wrapper = mount(Login, {
localVue,
propsData: { age }
})
// mock 一个函数
let loginFn = jest.fn((res)=>{
return res
})
// 监听 login 事件
wrapper.vm.$on('login',loginFn)
// 提交表单
wrapper.findComponent(Button).trigger('click')
// 断言 login 函数被调用
expect(loginFn).toBeCalled()
// 断言调用了一次 login 函数
expect(loginFn.mock.calls.length).toBe(1)
// 断言表单的对象为:{"age":100}
expect(loginFn.mock.results[0].value).toEqual({"age":100})
})
it('success submit form 第二方法', () => {
const age = 100
// 只能使用 mount 挂载组件
const wrapper = mount(Login, {
localVue,
propsData: { age }
})
// mock 一个函数
let loginFn=jest.spyOn(wrapper.vm, 'login')
// 重新实现该函数
loginFn.mockImplementationOnce((res)=>{
return res
})
// 提交表单
wrapper.findComponent(Button).trigger('click')
// 断言 login 函数被调用
expect(loginFn).toBeCalled()
// 断言调用了一次 login 函数
expect(loginFn.mock.calls.length).toBe(1)
// 断言表单的对象为:{"age":100}
expect(loginFn.mock.results[0].value).toEqual({"age":100})
})
it('error submit form', (done) => {
// 只能使用 mount 挂载组件
const wrapper = mount(Login, {
localVue,
propsData: { }
})
let loginElm = wrapper.vm.$el;
// mock 一个函数
let loginFn=jest.spyOn(wrapper.vm, 'login')
// 重新实现该函数
loginFn.mockImplementationOnce((res)=>{
return res
})
// 提交表单
wrapper.findComponent(Button).trigger('click')
wrapper.vm.$nextTick(()=>{
// 断言 login 函数被调用
expect(loginFn).not.toBeCalled()
// 断言 表单验证不通过
expect(loginElm.querySelector('.el-form-item__error').innerHTML).toContain('年龄不能为空')
// 结束断言测试
done();
})
});
it('reset form', () => {
// 只能使用 mount 挂载组件
const wrapper = mount(Login, {
localVue,
propsData: { }
})
// 给表单输入值
wrapper.vm.initForm(100)
let loginElm = wrapper.vm.$el;
// 点击重置表单
// loginElm.querySelectorAll('.el-button')[1].click() // ok
wrapper.findAllComponents(Button).at(1).trigger('click')
// 断言
expect(wrapper.vm.numberValidateForm.age).toBe('')
expect(loginElm.querySelector('.el-input__inner').value).toBe('')
});
})
执行单元测试: npm run test:unit
> vue-test-demo@0.1.0 test:unit /Users/liujun/Documents/huayun/test/vue-test-demo
> vue-cli-service test:unit
PASS tests/unit/example.spec.js
PASS tests/unit/helloworld.spec.js
PASS tests/unit/login.spec.js
----------------|----------|----------|----------|----------|-------------------|
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s |
----------------|----------|----------|----------|----------|-------------------|
All files | 100 | 100 | 100 | 100 | |
HelloWorld.vue | 100 | 100 | 100 | 100 | |
Login.vue | 100 | 100 | 100 | 100 | |
----------------|----------|----------|----------|----------|-------------------|
Test Suites: 4 passed, 4 total
Tests: 22 passed, 22 total
Snapshots: 0 total
Time: 13.359s
Ran all test suites.
4.编写 login.spec.js (不使用@vue/test-utils测试库 )
import { createTest, createVue, destroyVM } from './utils';
import Login from '@/components/Login.vue'
describe('Login', () => {
let vm;
afterEach(() => {
destroyVM(vm);
});
it('create', () => {
vm = createTest(Login, {}, true);
let loginElm = vm.$el;
expect(loginElm.querySelector('.el-button').innerHTML).toMatch('提交')
});
it('age', () => {
let age = 100
vm = createTest(Login, {
age
}, true);
let loginElm = vm.$el;
expect(loginElm.querySelector('.el-input__inner').value).toBe(age+"")
});
it('age 方式二', () => {
vm = createVue({
template: `
`,
components: {
Login
}
}, true);
let loginElm = vm.$el;
expect(loginElm.querySelector('.el-input__inner').value).toBe("100")
});
it('success submit form', () => {
let age = 100
vm = createTest(Login, {
age
}, true);
let loginElm = vm.$el;
// mock 一个函数
let loginFn = jest.fn((res)=>{
return res
})
// 监听 login 事件
vm.$on('login',loginFn)
// 点击提交表单
loginElm.querySelector('.el-button').click()
// 断言 login 函数被调用
expect(loginFn).toBeCalled()
// 断言调用了一次 login 函数
expect(loginFn.mock.calls.length).toBe(1)
// 断言表单的对象为:{"age":100}
expect(loginFn.mock.results[0].value).toEqual({"age":100})
});
it('error submit form', (done) => {
vm = createTest(Login, {}, true);
let loginElm = vm.$el;
// mock 一个函数
let loginFn = jest.fn((res)=>{
return res
})
// 监听 login 事件
vm.$on('login',loginFn)
// 点击提交表单
loginElm.querySelector('.el-button').click()
setTimeout(()=>{
// 断言 login 函数没有被调用
// expect(loginFn).not.toBeCalled() // ok
// 断言 表单验证不通过
expect(loginElm.querySelector('.el-form-item__error').innerHTML).toContain('年龄不能为空')
// 结束断言测试
done();
},20)
});
it('reset form', () => {
vm = createTest(Login, {}, true);
// 给表单输入值
vm.initForm(100)
let loginElm = vm.$el;
// 点击重置表单
loginElm.querySelectorAll('.el-button')[1].click()
// 断言
expect(vm.numberValidateForm.age).toBe('')
expect(loginElm.querySelector('.el-input__inner').value).toBe('')
});
})
5.utils.js 测试工具
// import Vue from 'vue';
import Vue from 'vue/dist/vue.js'; // 包含模板的编译模块
// 全局注册 element 所有的组件
import Element from 'element-ui'
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-testing.html#Writing-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);
执行单元测试: npm run test:unit
> vue-test-demo@0.1.0 test:unit /Users/liujun/Documents/huayun/test/vue-test-demo
> vue-cli-service test:unit
PASS tests/unit/example.spec.js
PASS tests/unit/helloworld.spec.js
PASS tests/unit/login.spec.js
----------------|----------|----------|----------|----------|-------------------|
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s |
----------------|----------|----------|----------|----------|-------------------|
All files | 100 | 100 | 100 | 100 | |
HelloWorld.vue | 100 | 100 | 100 | 100 | |
Login.vue | 100 | 100 | 100 | 100 | |
----------------|----------|----------|----------|----------|-------------------|
Test Suites: 4 passed, 4 total
Tests: 22 passed, 22 total
Snapshots: 0 total
Time: 13.359s
Ran all test suites.
参考文章:
https://jestjs.io/docs/en/expect
https://vue-test-utils.vuejs.org/zh/api/wrapper/#%E5%B1%9E%E6%80%A7
https://wangtunan.github.io/blog/test/vueTest.html#%E6%B5%8B%E8%AF%95%E4%BB%8B%E7%BB%8D