单元测试之jest
jest是Facebook的一套开源的JavaScript测试框架,它集成了快照测试、断言、mock以及覆盖率报告等功能,很全面而且基本不需要太多的配置便可使用Vue-Test-Utils是Vue的官方的单元测试框架,它提供了一系列非常方便的工具,使我们更加轻松的为Vue构建的应用来编写单元测试。
这里讲的主要是Vue+Jest+Vue-Test-Utils的项目,假设现在你已经使用vue-cli3搭建了一个vue项目:
1.安装jest
npm install --save-dev jest @vue/test-utils
//package.json
"scripts": {
"test": "jest",
}
2.vue-jest
vue-jest是一个 预处理器,如果不安装vue-jest,jest无法处理.vue
npm install --save-dev vue-jest
3.配置jest
在src/test目录新建jest.conf.js配置文件,目录如下:
我的配置内容如下:
const path = require('path'); module.exports = { verbose: true, testURL: 'http://localhost/', rootDir: path.resolve(__dirname, '../../../'), moduleFileExtensions: [ 'js', 'json', 'vue', ], testMatch: [ // 匹配测试用例的文件 '<rootDir>/src/test/unit/specs/*.spec.js', ], transform: { '^.+\\.js$': 'babel-jest', '.*\\.(vue)$': 'vue-jest', }, testPathIgnorePatterns: [ '<rootDir>/test/e2e', ], moduleNameMapper: { '^@/(.*)$': '<rootDir>/src/$1', }, };
4.配置之后就可以开始写测试用例了
在spec目录下新建一个ATest.spec.js文件(jest的测试脚本为.spec.js为后缀)
//ATest.vue组件 <template> <div> <Checkbox class="checkAlls" @on-change="checkboxChange" v-model="checkAll">全选</Checkbox> </div> </template> <script> export default { name: 'ATest', data() { return { checkAll: false, }; }, methods: { checkboxChange(value) { this.$emit('on-change', value); }, }, }; </script>
ATest.spec.js测试文件
import { mount, createLocalVue } from '@vue/test-utils';
import ATest from '@/components/ATest.vue';
import iviewUI from 'view-design';
const localVue = createLocalVue();
localVue.use(iviewUI);
describe('ATest.vue',()=>{
const wrapper =mount(ATest, {
localVue
});
it("事件被正常触发",()=>{
const stub = jest.fn();
wrapper.setMethods({ checkboxChange: stub });
//触发自定义事件
wrapper.find(".checkAlls").vm.$emit("on-change");
wrapper.setData({checkAll: true});
expect(wrapper.vm.checkAll).toBe(true);
})
})
5.vue-test-utils常用的API
-
mount()
创建一个包含被挂载和渲染的 Vue 组件的 wrapper,它仅仅挂载当前实例
-
shallowMount()
和 mount 一样,创建一个包含被挂载和渲染的 Vue 组件的 Wrapper,只挂载一个组件而不渲染其子组件 (即保留它们的存根),这个方法可以保证你关心的组件在渲染时没有同时将其子组件渲染,避免了 子组件可能带来的副作用(比如Http请求等)
mount和shallowMount区别的案例解释
//App.vue <template> <div id="app"> <Page :messages="messages"></Page> </div> </template>
//子组件 <template> <div> <p v-for="message in messages" :key="message">{{message}}</p> </div> </template>
//测试用例App.spec.js
import { mount } from 'vue-test-utils';
import App from '@/App';
describe('App.test.js', () => {
let wrapper;
let vm;
beforeEach(() => {
wrapper = mount(App);
vm = wrapper.vm;
wrapper.setProps({ messages: ['Cat'] })
});
// 为App的单元测试增加快照(snapshot):
it('has the expected html structure', () => {
expect(vm.$el).toMatchSnapshot()
})
});
执行单元测试后,测试通过,然后Jest会在test/__snapshots__/文件夹下创建一个快照文件App.spec.js.snap
exports[`App.test.js has the expected html structure 1`] =
` <div id="app" > <div> <p> Cat </p> </div> </div> `;
//通过快照我们可以发现,子组件Test1被渲染到App中了。
将App.spec.js中的mount方法更改为shallow方法,再次查看快照
exports[`App.test.js has the expected html structure 1`] =
` <div id="app" > <!----> </div> `;
//可以看出来,子组件没有被渲染
该案例的详细解释可以看这篇文章: https://blog.csdn.net/duola8789/article/details/80434962
-
createLocalVue
返回一个 Vue 的类供你添加组件、混入和安装插件而不会污染全局的 Vue 类
import { createLocalVue, shallowMount } from '@vue/test-utils'import Foo from './Foo.vue'
const localVue = createLocalVue()const wrapper = shallowMount(Foo, {
localVue,
mocks: { foo: true }})expect(wrapper.vm.foo).toBe(true)
const freshWrapper = shallowMount(Foo)expect(freshWrapper.vm.foo).toBe(false)
-
选择器 (详细看Vue-Test-Utils官网介绍)
-
$route 和 $router
import { mount} from '@vue/test-utils'
import Test from '@/components/common/Test.vue';
describe('Test.vue',()=>{
const wrapper = mount(Test, {
mocks: {
$route: { path: '/login' }
}
})
it("test", ()=>{
expect(wrapper.vm.$route.path).toMatch('/login')
})
})
-
状态管理Vuex
import Vuex from 'vuex';
import {mount, createLocalVue} from '@vue/test-utils';
const localVue = createLocalVue();
localVue.use(Vuex);
describe('Test.vue',()=>{
let wrapper;
let getters;
let store;
let action;
let mutations;
let option;
beforeEach(()=>{
state = {
name: '张三'
}
getters = {
GET_NAME: ()=> '张三'
}
mutations = {
SET_NAME: jest.fn()
}
action = {
setName: jest.fn()
}
store = new Vuex.Store({
getters,
mutations,
action
})
option = {
store,
localVue
}
wrapper = mount(Test, option)
})
it("测试vuex", ()=>{
expect(getters.GET_Name()).toEqual('张三');
wrapper.vm.$store.state.name= "李四";
expect(wrapper.vm.name).toMatch('李四');
wrapper.find('.btn').trigger('click')
expect(actions.setName).toHaveBeenCalled()
wrapper.find({ref: 'testComp'}).vm.$emit('on-select');
expect(mutations.SET_NAME).toBeCalled()
})
})
-
wrapper
一个 Wrapper 是一个包括了一个挂载组件或 vnode,以及测试该组件或 vnode 的方法
属性
setMethods: 设置 Wrapper vm 的方法并强制更新。 通过setMethods方法用mock函数代替真实的方法,然后就可以断言点击按钮后对应的方法有没有被触发、触发几次、传入的参数等等
6.测试钩子
-
beforeEach(fn) 在每一个测试之前需要做的事情,比如测试之前将某个数据恢复到初始状态
-
afterEach(fn) 在每一个测试用例执行结束之后运行
-
beforeAll(fn) 在所有的测试之前需要做什么
-
afterAll(fn) 在测试用例执行结束之后运行
他们的调用顺序为: beforeAll => beforeEach => afterAll => afterEach
7.模拟接口请求
//Test.vue
mounted() {
this.getDataList();
},
methods: {
getDataList() {
this.$api.data.getData().then((res) => {
this.List= res.data;
});
}
import {shallowMount, createLocalVue} from '@vue/test-utils';
const localVue = createLocalVue();
//axios请求
jest.mock('../data.js', ()=>({
getData: () => Promise.resolve({data:{name:'张三'}})
}))
describe('Test.vue',()=>{
const option;
let wrapper =shallowMount(Test, option)
it("异步接口被正常执行", async()=>{
const getDataList= jest.fn();
option.methods = {getDataList};
shallowMount(Test, option);
await expect(getDataList).toBeCalled();
})
it('测试异步接口的返回值', () => {
return localVue.nextTick().then(() => {
expect(wrapper.vm.List).toEqual( {name:'张三'});
})
})
})
7.常见的报错
[vue-test-utils]: find did not return .btn, cannot call trigger() on empty Wrapper
//出现该问题的时候,除了要确保你的组件确实存在该类名的情况,还要确保存在v-if的时候是否为true,如果为false,只需要在单测里将其设置为true,该问题便可解决
"ReferenceError: sessionStorage is not defined"
//出现该问题只需要去模拟本地存储就可以了,npm提供了一个模拟本地存储数据的依赖包mock-local-storage,安装后在单测文件里导入即可
//import 'mock-local-storage';
//包地址:https://www.npmjs.com/package/mock-local-storage
TypeError: Cannot read property xxxx of undefined
//这个问题的一般解决方法是直接mock数据
wrapper = mount(Test, {
mocks:{
$router:[], //不然可能会报 Cannot read property 'push' of undefined,
$cacheKeys: { TOKEN: 1 }, // Cannot read property 'TOKEN' of undefined
}
})
注:要明白测试的目的,测试关心是我们的代码有没有达到我们预期的效果,它并不关心实现的过程,所以不需要太过于纠结变量的取值或者其他的问题
8、覆盖率报告
单元测试有四个指标:
-
%stmts是语句覆盖率(statement coverage):是否每个语句都执行了?
-
%Branch分支覆盖率(branch coverage):是否每个if代码块都执行了?
-
%Funcs函数覆盖率(function coverage):是否每个函数都调用了?
-
%Lines行覆盖率(line coverage):是否每一行都执行了?
jest提供了生成测试覆盖率报告的命令
- npx jest --init 生成配置文件jest.config.js
- package.json添加上
--coverage
这个参数
//修改package.json
"scripts": {
"test": "jest --coverage"
}
npm run test之后会生成coverage文件
然后再网页打开index.html,就会看到下图
三种颜色分别代表不同比例的覆盖率(<50%红色,50%~80%灰色, ≥80%绿色)
点击文件名可以查看代码的执行情况,
旁边显示的1x代表执行的次数
vue-jest-utils官网:https://vue-test-utils.vuejs.org/zh/guides/
单元测试的相关参考链接:https://alexjover.com/blog/