# mocha 单元测试
## 前言
**单元测试** 是用来对 **一个模块**、 **一个函数** 或 **一个类** 来进行正确性检验的测试工作。
>比如对函数abs(),可以便携以下几个 **测试用例**。
1. 输入正数,比如8、 8.8、 0.88,期待返回值与输入相同;
2. 输入负数,比如-8、 -8.8、 -0.88,期待返回值与输入相反;
3. 输入0,期待返回0;
4. 输入非数值类型(如null、{}、[]),期待抛出Error。
>把上面的测试用例放到一个测试模块里,就是对于这个函数的单元测试。
>如果单元测试通过,说明测试的这个函数能正常工作。如果单元测试不通过,要么函数有bug,要么测试条件输入不正确,总之需要修改代码,通过单元测试才行。
>单元测试通过的意义在于,如果有人修改的abs()函数,只需要再跑一遍该函数的单元测试,如果通过,说明此次修改不会对函数原有的行为造成影响,如果测试不通过,说明修改有问题。
## 开始
### 创建带有mocha的vue项目
>这里使用的是vue-cli3.0的版本
1. 运行vue create xxx命令;
2. 在预设中选择Unit Testing;
3. 然后在选择测试方案中选择Mocha + Chai;
3. 等待安装完毕即可。
>运行测试
1. **文件目录**
```js
|-- 根目录
| |-- tests
| | |-- unit
| | | --example.spec.js
```
* 测试用例文件名要以.spec.js结尾
>举个
```js
import { expect } from 'chai';
import { shallowMount } from '@vue/test-utils';
import HelloWorld from '@/components/HelloWorld.vue';
describe('HelloWorld.vue', () => {
it('renders props.msg when passed', () => {
const msg = 'new message';
const wrapper = shallowMount(HelloWorld, {
propsData: { msg },
});
expect(wrapper.text()).to.include(msg);
});
});
```
2. **执行测试**
```js
npm run test:unit
```
* 控制台打印结果如下:
```js
HelloWorld.vue
√ renders props.msg when passed (44ms)
1 passing (44ms)
MOCHA Tests completed successfully
```
### 已有vue项目添加单元测试
> 可使用命令:
```js
vue add @vue/unit-mocha
```
这样就和在一开始使用cli创建vue项目时选择mocha测试一致了。并且,它还会生成tests/unit/example.spec.js这个测试用例,npm命令也会支持test:unit命令。
>或者你可以直接安装@vue/cli-plugin-unit-mocha:
```js
npm install --save-dev @vue/cli-plugin-unit-mocha
```
> 不过需要做一些额外操作:
1. 自行在根目录下创建tests/unit及测试用例
2. 手动在package.js的script字段中增加"test:unit": "vue-cli-service test:unit"命令
## mocha 钩子函数
> mocha 在 **describe** 块中,提供了四个钩子函数,如下:
- before()
- after()
- beforeEach()
- afterEach()
```js
describe('hooks', function() {
before(function() {
// 在本区块的所有测试用例之前执行
});
after(function() {
// 在本区块的所有测试用例之后执行
});
beforeEach(function() {
// 在本区块的每个测试用例之前执行
});
afterEach(function() {
// 在本区块的每个测试用例之后执行
});
// test cases
});
```
>举个
```js
describe('example for beforeEach', () => {
var foo = false;
beforeEach(() => {
foo = true;
});
it('success to change the global variables', () => {
expect(foo).to.be.equal(true);
});
});
```
> beforeEach会在it之前执行:
```js
describe('asynchronous operation example for beforeEach', () => {
var foo = false;
beforeEach((done) => {
setTimeout(() => {
foo = true;
done();
}, 500);
});
it('success to change the global variables', () => {
expect(foo).to.be.equal(true);
});
});
```
* beforeEach会在it之前执行,即使里面是异步操作,也会先执行,所以会修改全局变量成功。
## chai
**chai** 是一个第三方提供的断言库,提供有 **assert**、**expect**、**should** 三种风格。
### assert 断言
**assert** 模块提供了断言测试的函数,用于测试不变式。有 **strict** 和 **legacy** 两种模式,建议只使用 **strict** 模式。
#### 常用API
##### assert(value[, message]);
> 检查 value 是否为真
```js
assert(0); // false
assert(1); // true
```
##### assert(expression[, message]);
> 检查 自定义表达式 是否为真
```js
assert('foo' == 'bar'); // false
assert('foo' != 'foo'); // false
assert('foo' !== 'bar'); // true
```
##### assert.equal(actual, expected[, message]);
> 相当于使用 == 运算符比较两个参数值 actual 和 expected 是否相等
```js
assert.equal(1 + 1, 2); // true
assert.equal(1 + 1, 3); // false
assert.equal([1, 2], [1, 2]); // false
assert.equal({a: 1}, {a: 1}); // false
```
##### assert.strictEqual(actual, expected[, message]);
> equal() 的严格模式,相当于使用 === 运算符
```js
assert.strictEqual(1 + 1, 2); // true
assert.equstrictEqualal(1 + 1, '2'); // false
```
##### assert.strictEqual(actual, expected[, message]);
> equal() 的严格模式,相当于使用 === 运算符
```js
assert.strictEqual(1 + 1, 2); // true
assert.strictEqual(1 + 1, '2'); // false
```
##### assert.deepEqual(actual, expected[, message]);
> 深度比较
```js
assert.deepEqual([1, 2], [1, 2]); // true
assert.deepEqual({a: 1}, {a: 1}); // true
assert.deepEqual(1 + 1, 2); // true
assert.deepEqual({a: 1}, {a: 2}); // false
```
##### assert.deepStrictEqual(actual, expected[, message]);
> 深度比较的严格版本
```js
assert.deepStrictEqual([1, 2], [1, 2]); // true
assert.deepStrictEqual([1, 2], [1, '2']); // false
```
##### assert.notEqual(actual, expected[, message]);
> 相当于 != 运算符
```js
assert.notEqual(1 + 1, 3); // true
assert.notEqual(1 + 1, '2'); // false
```
##### assert.notStrictEqual(actual, expected[, message]);
> 相当于 !== 运算符
```js
assert.notStrictEqual(1 + 1, 3); // true
assert.notStrictEqual(1 + 1, '2'); // true
```
### expect 断言
#### 语言链
下面是提供的一些有助于断言可读性的语言链
- to
- be
- been
- is
- that
- which
- and
- has
- have
- with
- at
- of
- same
- but
- does
>举几个
```js
expect(1).to.equal(1); // true
expect(1).to.not.equal(2); // true
expect({a: 1}).to.have.property('a'); // true
expect({b: 1}).to.not.have.property('b'); // true
expect(1).to.be.a('number'); // true
expect([1, 2]).to.be.an('array'); // true
expect({a: 1}).to.deep.equal({a: 1}); // true
expect({a: 1}).to.not.equal({a: 1}); // true
```
## mocha 应用中 所遇到的问题
### 1. sass-loader问题
***已有项目引入单元测试后,sass-loader无法正确加载neo组件库的样式文件,并报找不到相关模块的错误***
> 相关报错如下:
```js
Module build failed (from ./node_modules/sass-loader/lib/loader.js):
Can't find stylesheet to import.
╷
│ @import "./functions";
│ ^^^^^^^^^^^
╵
```
> 解决办法:引入 null-loader
```js
module.exports = {
chainWebpack: (config) => {
if (process.env.NODE_ENV === 'test') {
const scssRule = config.module.rule('scss');
scssRule.uses.clear();
scssRule
.use('null-loader')
.loader('null-loader');
}
}
```
* [github 上相关issue地址](https://github.com/vuejs/vue-cli/issues/4053)
* [可参考 vue-cli 官网地址](https://cli.vuejs.org/guide/webpack.html#chaining-advanced)
### 2. 测试组件中的 Vuex
***如:kde-esalesins-vue项目中的完善信息页面(src/module/proposal/view/logClientInfo.vue),对此页面的相关校验规则做单元测试时,出现了以下问题:***
> 相关报错如下:
```js
logClientInfo.vue
[Vue warn]: Error in created hook: "TypeError: Cannot read property 'dispatch' of undefined"
found in
--->
TypeError: Cannot read property 'dispatch' of undefined
...
...
[Vue warn]: Error in render: "TypeError: Cannot read property 'getters' of undefined"
found in
--->
TypeError: Cannot read property 'getters' of undefined
...
...
```
1. vue-test-utils 提供了相关的API(createLocalVue),即可解决上述报错,代码如下:
```js
import { expect } from 'chai';
import { shallowMount, createLocalVue } from '@vue/test-utils';
import LogClientInfo from '@/module/proposal/view/logClientInfo.vue';
import Vuex from 'vuex';
import sinon from 'sinon';
import { queryClientInfo } from '@/store/common/actions';
const localVue = createLocalVue();
localVue.use(Vuex);
const appClient = {
clientBirthdayStr: '1961-09-16',
clientName: '三审',
clientNo: '92A38900FC1A6D07E05490E2BADCE89C',
feeCurrAge: '7',
isSameClient: '',
maxData: 'on Sep 15 2003 08:0:0 GMT+0800 (中国标准时间)',
professionCode: '',
professionName: '',
relationShip: '3',
relationShipOther: '',
relationShipText: '父母',
sex: 'M',
};
describe('logClientInfo.vue', () => {
let getters;
let actions;
let store;
let wrapper;
beforeEach(() => {
getters = {
documentId: () => '92A381180E0D4600E05490E2BADCE89C1',
appClient: () => appClient,
insClient: () => null,
isReserve: () => 'Y',
otherClient: () => null,
trustPlan: () => 'Y',
};
actions = {
queryClientInfo: sinon.spy(),
};
store = new Vuex.Store({
state: {},
getters,
actions,
});
wrapper = shallowMount(LogClientInfo, { store, localVue });
});
it('Renders store.getters', () => {
const appName = wrapper.find('.infoBox .userName');
expect(appName.text()).to.equal(getters.appClient.clientName);
});
});
```
2. 上述代码生效后,开始报以下错误...
```js
ErrorWrapper { selector: '.infoBox .userName' }
1) Renders store.getters
14 passing (447ms)
1 failing
1) logClientInfo.vue
Renders store.getters:
Error: [vue-test-utils]: find did not return .infoBox .userName, cannot call text() on empty Wrapper
```
3. 以上报错原因:本页面使用了自定义组件和neo组件,组件内的元素无法选中,使用 shallowMount 即可
涉及到 mount 和 shallowMount 的区别:
* mount 是完整的渲染
* shallowMount 渲染的子组件是假的,也就是只mount了这一层
```js
beforeEach(() => {
...
wrapper = mount(LogClientInfo, {
localVue,
store,
stubs: ['registered-component'],
attachToDocument: true, // 当设为 true 时,组件在渲染时将会挂载到 DOM 上。
});
};
```
* [Vue 组件的单元测试 网址](https://cn.vuejs.org/v2/cookbook/unit-testing-vue-components.html)
* [vue-test-utils 官网地址(配合vuex)](https://vue-test-utils.vuejs.org/zh/guides/using-with-vuex.html)
* [vue-test-utils 官网地址(挂载选项)](https://vue-test-utils.vuejs.org/zh/api/options.html)
## 其他测试辅助工具
### Sinon
Sinon是用来辅助我们进行前端测试的,在我们的代码需要与其他系统或者函数对接时,它可以模拟这些场景,从而使我们测试的时候不再依赖这些场景。
Sinon主要有两种玩法:
1. 模拟函数
```js
import {assert} from 'chai'
import sinon from 'sinon'
import once from '../src/once'
describe('测试Once函数', function () {
it('传入Once的函数会被调用', function () {
var callback = sinon.spy();
var proxy = once(callback);
proxy();
assert(callback.called);
});
})
```
2. 监控原有函数
```js
it('对原有函数的spy封装,可以监听原有函数的调用情况', function () {
const obj={
func:()=>{
return 1+1
}
}
sinon.spy(obj,'func')
obj.func(3);
assert(obj.func.calledOnce)
assert.equal(obj.func.getCall(0).args[0], 3);
});
```
Sinon有主要有三个方法辅助我们进行测试:spy,stub,mock。
1. spy生成一个间谍函数,它会记录下函数调用的参数,返回值,this的值,以及抛出的异常。
```js
import { assert } from 'chai';
const func = sinon.spy();
func();
assert(func.calledOnce);
```
2. stub是带有预编程行为的函数。
简单点说,就是spy的加强版,不仅完全支持spy的各种操作,还能操作函数的行为。
和spy一样,stub也能匿名,也能去封住并监听已有函数。
然而有一点和spy不同,当封装了一个已有函数后,原函数不会再被调用。
```js
it('对原有函数的stub封装,可以监听原有函数的调用情况,以及模拟返回', function () {
const obj={
func:()=>{
console.info(1)
}
}
sinon.stub(obj,'func').returns(42)
const result=obj.func(3);
assert(obj.func.calledOnce)
assert.equal(obj.func.getCall(0).args[0], 3);
assert.equal(result,43);
});
```
3. mock能对多个函数进行监听和拦截,并且mock没有得到期望的结果就会测试失败
```js
it('mock的测试', function () {
var myAPI = {
method: function () {
console.info("运行method")
},
func: function () {
console.info("运行method")
}
};
var mock = sinon.mock(myAPI);
mock.expects("method").once().returns(2);
mock.expects("func").twice()
myAPI.method();
myAPI.func();
myAPI.func();
mock.verify();
});
```
### mock-browser
模拟的浏览器,可以模拟window, document, location, navigation, local and session storage等全局变量
```js
import mockBrowser from 'mock-browser';
describe('proposal.vue', () => {
before(() => {
const MockBrowser = mockBrowser.mocks.MockBrowser;
global.sessionStorage = new MockBrowser().getSessionStorage();
});
})
```
* [mock-browser 用法指南](https://www.npmjs.com/package/mock-browser)
## 配合vuex进行单测
个人观点,如有不同意见请一起探讨,谢谢~
### 模拟state
```js
const state = {
insClient: {},
appClient: {},
otherClient: {},
relationShip: '',
relationShipOther: '',
documentId: '',
statuCode: '',
};
```
### 模拟mutations
```js
const mutations = {
SAVE_INSURANCE_INFO: sinon.spy(Mut.SAVE_INSURANCE_INFO),
SAVE_STATUCODE: sinon.spy(Mut.SAVE_STATUCODE),
SAVE_INS_CLIENT_SEX: sinon.spy(Mut.SAVE_INS_CLIENT_SEX)
};
```
### 模拟actions
模拟一个返回Promise函数的方法可以触发回调
```js
const actions = {
queryInsuranseInfo: sinon.spy(() => new Promise((resolve) => {
resolve(info);
})),
};
```
### 模拟getters
```js
const getters = {
insClient: () => state.insClient,
appClient: () => state.appClient,
otherClient: () => state.otherClient,
relationShip: () => state.relationShip,
relationShipOther: () => state.relationShipOther,
documentId: () => state.documentId,
};
```
### 模拟store
将模拟的各模块注册到store中,挂载在组件上。
```js
import proposal from '@/module/proposal/view/proposal.vue';
import Vuex from 'vuex';
import { createLocalVue, mount } from '@vue/test-utils';
const localVue = createLocalVue();
localVue.use(Vuex);
store = new Vuex.Store({
state,
actions,
mutations,
getters,
});
mount(proposal, { store, localVue });
```
## 其他问题
### 使用$route
相关报错如下:
```js
logClientInfo.vue
[Vue warn]: Error in data(): "TypeError: Cannot read property 'query' of undefined"
found in
--->
TypeError: Cannot read property 'query' of undefined
at VueComponent.data (/Users/ex-zhouying006/projects/kde-esalesins-vue/dist/1309/webpack:/logClientInfo.vue:145:1)
```
组件内部需要用到$route,可以自己模拟一个$route挂载到实例上
```js
import { createLocalVue, mount } from '@vue/test-utils';
import proposal from '@/module/proposal/view/proposal.vue';
const $route = {
query: {},
};
const wrapper = mount(proposal, { localVue, mocks: { $route } });
```
### 异步渲染测试
Promise函数返回的数据渲染测试
1. 计时器: 异步测试需要在测试完成的时候执行done函数的回调。
```js
it('proposal info render', (done) => {
const $route = {
query: {},
};
const wrapper = mount(proposal, { store, localVue, mocks: { $route } });
mutations.SAVE_INSURANCE_INFO(state, info);
expect(wrapper.find('.sex .active').text()).to.equal('女');
expect(wrapper.find('.age').text()).to.equal('30生日(选填)');
setTimeout(() => {
expect(wrapper.find('.toInfo').text()).to.equal('完善信息');
done();
}, 1000);
});
```
2. nextTick结合async
```js
it('proposal info render', async () => {
// const wrapper = new Vue(proposal).$mount();
mutations.SAVE_INSURANCE_INFO(state, info);
expect(wrapper.find('.sex .active').text()).to.equal('女');
expect(wrapper.find('.age').text()).to.equal('30生日(选填)');
await wrapper.vm.$nextTick();
expect(wrapper.find('.toInfo').text()).to.equal('完善信息');
});
```
### 埋点方法的模拟
#### 有些同学调用埋点的方法时,可能没有判断类似TrackPoint.collectEvent的方法是否存在,单测的时候就会报错.
1. 当使用带window的时候
```js
window.TrackPoint.collectEvent({
pageNo:"", // 当前H5页面编号(自动补充)
eventId:"C01968", // 事件id
eventTitle:"E锦囊线索列表_线索评价", // 事件名称
eventTag:"", // 事件标签(暂时保留字段,可以传空)
parameters:{ // 扩展字段
点击时间: this.currentDate,
代理人: this.getAgentNo,
线索ID: this.salesLeadId,
位置: '(PJXS)',
version: '2.0'
}
})
```
此时需要在before钩子里在window里注入方法
```js
before(function() {
window.TrackPoint = {
pageExtendData: () => {},
pageStart: () => {},
pageChange: () => {},
collectEvent: () => {}
};
})
```
2. 当不带window的调用
```js
TrackPoint.collectEvent({
pageNo:"", // 当前H5页面编号(自动补充)
eventId:"C01968", // 事件id
eventTitle:"E锦囊线索列表_线索评价", // 事件名称
eventTag:"", // 事件标签(暂时保留字段,可以传空)
parameters:{ // 扩展字段
点击时间: this.currentDate,
代理人: this.getAgentNo,
线索ID: this.salesLeadId,
位置: '(PJXS)',
version: '2.0'
},
})
```
此时需要在before钩子里在globale里注入方法
```js
before(function() {
global.TrackPoint = {
pageExtendData: () => {},
pageStart: () => {},
pageChange: () => {},
collectEvent: () => {}
};
})
```
### Native方法的模拟
#### 当页面进入就会调用Native方法,单测的时候就会报错.比如页面进入就会获取代理人号
1. 这时候就需要模拟Native方法
```js
import Native from '@/util/native.js';
before(function() {
Native.getAgentNo = (success, error) => {
success({
agentNo: '1120103025'
});
};
})
```
### 模拟左划和右滑
#### 有的页面可以左划出另一个功能,比如E锦囊线索左划可以评论, 此时需要利用trigger模拟出touchStart和 touchEnd
1. 模拟事件的时候,可以模拟出Event对象
```js
const hdWarp = wrapper.find('.imgs-wrapper').find('.imgs-wrapper .hdWarp');
hdWarp.trigger('touchstart', {
changedTouches: [
{
pageX: 354
},
]
});
hdWarp.trigger('touchend', {
changedTouches: [
{
pageX: 300
},
]
});
expect(hdWarp.classes('move')).to.be.true;
```
### 模拟document选择器
#### 有的页面会在mounted里动态计算页面的高度,此时单测就会报错
1. 类似这种
```js
mounted() {
this.$nextTick(() => {
this.timeOut = setTimeout(() => {
const mainH = document.querySelector('.main-section').clientHeight;
const searchH = document.querySelector('.searchBar').clientHeight;
const headerH = document.querySelector('.pa-header').clientHeight;
// const tabbarH = `${mainH - searchH - headerH}px`;
// const containerH = document.querySelector('.matrix-tab__container').clientHeight;
const containerH = 0;
const lookMoreH = document.querySelector('.lookMore').clientHeight;
const nulldomH = document.querySelector('.nulldom').clientHeight;
document.querySelector('.matrix-slide').style.height = `${mainH - searchH - containerH - lookMoreH - nulldomH}px`;
document.querySelector('.matrix-scroll-list-wrapper').style.minHeight = `${mainH - searchH - containerH - lookMoreH - nulldomH + 1}px`;
const guideH = headerH + 40 + lookMoreH + 0.18 * parseFloat(document.documentElement.style.fontSize);
// 蒙层指示高度
this.guideH = guideH;
console.log('mounted未报错');
// 滚动监听
// const indexID = this.RecordItem(); // 返回曝光的线索ID
// this.RecordItems.push({ indexID, y: 0 });
}, 300);
// setTimeout(() => {
// this.RecordItem();
// },400)
});
},
```
此时就需要模拟document选择器
```js
before(function() {
save = sinon.stub(document, 'querySelector').returns({
clientHeight: 44,
style: {
height: 55,
minHeight: 55,
},
});
});
```
让 ``document.querySelector``直接返回它需要的值。
## 注意事项
1. mocha推荐我们最好不要用箭头函数,因为箭头函数会拿不到mocha的上下文this
2. 当我们不想测试某个案例的时候,最好用this.skip() 跳过他而不是注释掉代码
### 测试框架
```mocha``` 那么mocha是什么呢?他其实是一种JavaScript的一种单元测试框架,就是我们测试的代码需要运行在它提供的环境下(其实还有```jest```, ```Jasmine```, ```AVA```)
### 测试用具
```Vue Test Utils``` 他是Vue官方提供的一种针对Vue编写的应用,对需要测试的Vue组件做出各种操作的工具库。在没有这个工具之前我们的代码可能是这样的:
```js
import Vue from 'vue'
import HelloWorld from '@/components/HelloWorld'
describe('HelloWorld.vue', () => {
it('should render correct contents', () => {
const Constructor = Vue.extend(HelloWorld)
const vm = new Constructor().$mount()
expect(vm.$el.querySelector('.hello h1').textContent)
.toEqual('Welcome to Your Vue.js App')
})
})
```
但是出现了```Vue Test Utils```我们的代码是这样的:
```js
import { shallow } from '@vue/test-utils'
import HelloWorld from '@/components/HelloWorld'
describe('HelloWorld.vue', () => {
it('should render correct contents', () => {
const wrapper = shallow(HelloWorld, {
attachToDocument: ture
})
expect(wrapper.find('.hello h1').text()).to.equal('Welcome to Your Vue.js App')
})
})
```
代码更加简洁了。
### 断言库以及一些测试工具库
断言库其实就是为了让我们的断言程序变得更加语义化,它提供的```to.be.ok,to.not.be.ok,expect({a: 1, b: 2}).to.be.an('object').that.has.all.keys('a', 'b')```让我们能通过代码一眼看出来测试为了测试什么
让我们分析一下下面这段代码
```js
describe('Test', function() {
// 需要渲染出一个button
it('renders the correct markup', () => {
expect(wrapper.html()).contains('0');
});
it('按钮没有点击之前应该是0点击之后应该是1', () => {
const count = wrapper.find('span.count');
const button = wrapper.find('button');
expect(count.html()).contains('0');
expect(wrapper.vm.count).equal(0);
button.trigger('click');
console.log('count.........', wrapper.vm.count);
expect(wrapper.vm.count).equal(1);
});
});
```
```describe```, ```it``` 关键字是测试框架(```mocha```)提供的,```expect```,```equal``` 是断言库(```chai```)提供的,```wrapper.html()```是Vue测试工具库(vue-test-utils)提供的。
那么,```@vue/cli-plugin-unit-mocha``` 和 ```vue add @vue/unit-mocha``` 又是什么呢?
因为我们的项目是基于vue-cli3的,我们应该知道Vue 集成了一个命令``vue-cli-service`` 他就是Vue自己封装了一套webpack的命令 因为他最终还是用webpack的, @vue/cli-plugin-unit-mocha通过这个名字我们就应该知道,他是cli3自己集成的整合了mocha的的插件,不然我们单独安装mocha,还得需要配置其它的, @vue/unit-mocha也是如此,配置更充足,根本不需要你安装别的。这就是所谓的所见即所得。
### 覆盖率测试
什么是代码覆盖率: 即代码的覆盖程度。
众所周知,我们代码是由各种可执行语句和判断分支根据这些我们的覆盖分为了四种:
1. 行覆盖率(line coverage):是否每一行都执行了?
2. 函数覆盖率(function coverage):是否每个函数都调用了?
3. 分支覆盖率(branch coverage):是否每个if代码块都执行了?
4. 语句覆盖率(statement coverage):是否每个语句都执行了?
### 在我们的项目中添加覆盖率测试报告
#### 其实很简单,我们只需要安装nyc, nyc的前身就是Istanbul(以土耳其最大城市伊斯坦布尔命名,因为土耳其地毯世界闻名,而地毯是用来覆盖的。)
创建 scripts ```"coverage": "nyc --reporter=html --reporter=text npm run test:unit"```就行,具体配置可以查看[文档](https://istanbul.js.org/)
参考文档:
* [Vue 组件的单元测试 网址](https://cn.vuejs.org/v2/cookbook/unit-testing-vue-components.html)
* [vue-test-utils 官网地址(配合vuex)](https://vue-test-utils.vuejs.org/zh/guides/using-with-vuex.html)
* [vue-test-utils 官网地址(挂载选项)](https://vue-test-utils.vuejs.org/zh/api/options.html)