詳解Vue單元測(cè)試Karma+Mocha學(xué)習(xí)筆記
在使用vue-cli創(chuàng)建項(xiàng)目的時(shí)候,會(huì)提示要不要安裝單元測(cè)試和e2e測(cè)試。既然官方推薦我們使用這兩個(gè)測(cè)試框架,那么我們就動(dòng)手去學(xué)習(xí)實(shí)踐一下他們吧。
簡(jiǎn)介
Karma
Karma是一個(gè)基于Node.js的JavaScript測(cè)試執(zhí)行過(guò)程管理工具(Test Runner)。該工具在Vue中的主要作用是將項(xiàng)目運(yùn)行在各種主流Web瀏覽器進(jìn)行測(cè)試。
換句話說(shuō),它是一個(gè)測(cè)試工具,能讓你的代碼在瀏覽器環(huán)境下測(cè)試。需要它的原因在于,你的代碼可能是設(shè)計(jì)在瀏覽器端執(zhí)行的,在node環(huán)境下測(cè)試可能有些bug暴露不出來(lái);另外,瀏覽器有兼容問(wèn)題,karma提供了手段讓你的代碼自動(dòng)在多個(gè)瀏覽器(chrome,firefox,ie等)環(huán)境下運(yùn)行。如果你的代碼只會(huì)運(yùn)行在node端,那么你不需要用karma。
Mocha
Mocha是一個(gè)測(cè)試框架,在vue-cli中配合chai斷言庫(kù)實(shí)現(xiàn)單元測(cè)試。
而Chai斷言庫(kù)可以看Chai.js斷言庫(kù)API中文文檔,很簡(jiǎn)單,多查多用就能很快掌握。
我對(duì)測(cè)試框架的理解
npm run unit 執(zhí)行過(guò)程
- 執(zhí)行 npm run unit 命令
- 開(kāi)啟Karma運(yùn)行環(huán)境
- 使用Mocha去逐個(gè)測(cè)試用Chai斷言寫(xiě)的測(cè)試用例
- 在終端顯示測(cè)試結(jié)果
- 如果測(cè)試成功,karma-coverage 會(huì)在 ./test/unit/coverage 文件夾中生成測(cè)試覆蓋率結(jié)果的網(wǎng)頁(yè)。
Karma
對(duì)于Karma,我只是了解了一下它的配置選項(xiàng)。
下面是Vue的karma配置,簡(jiǎn)單注釋了下:
var webpackConfig = require('../../build/webpack.test.conf')
module.exports = function (config) {
config.set({
// 瀏覽器
browsers: ['PhantomJS'],
// 測(cè)試框架
frameworks: ['mocha', 'sinon-chai', 'phantomjs-shim'],
// 測(cè)試報(bào)告
reporters: ['spec', 'coverage'],
// 測(cè)試入口文件
files: ['./index.js'],
// 預(yù)處理器 karma-webpack
preprocessors: {
'./index.js': ['webpack', 'sourcemap']
},
// Webpack配置
webpack: webpackConfig,
// Webpack中間件
webpackMiddleware: {
noInfo: true
},
// 測(cè)試覆蓋率報(bào)告
// https://github.com/karma-runner/karma-coverage/blob/master/docs/configuration.md
coverageReporter: {
dir: './coverage',
reporters: [
{ type: 'lcov', subdir: '.' },
{ type: 'text-summary' }
]
}
})
}
Mocha和chai
我們看下官方的例子(都用注釋來(lái)解釋代碼意思了):
import Vue from 'vue' // 導(dǎo)入Vue用于生成Vue實(shí)例
import Hello from '@/components/Hello' // 導(dǎo)入組件
// 測(cè)試腳本里面應(yīng)該包括一個(gè)或多個(gè)describe塊,稱為測(cè)試套件(test suite)
describe('Hello.vue', () => {
// 每個(gè)describe塊應(yīng)該包括一個(gè)或多個(gè)it塊,稱為測(cè)試用例(test case)
it('should render correct contents', () => {
const Constructor = Vue.extend(Hello) // 獲得Hello組件實(shí)例
const vm = new Constructor().$mount() // 將組件掛在到DOM上
//斷言:DOM中class為hello的元素中的h1元素的文本內(nèi)容為Welcome to Your Vue.js App
expect(vm.$el.querySelector('.hello h1').textContent)
.to.equal('Welcome to Your Vue.js App')
})
})
需要知道的知識(shí)點(diǎn):
- 測(cè)試腳本都要放在 test/unit/specs/ 目錄下。
- 腳本命名方式為 [組件名].spec.js。
- 所謂斷言,就是對(duì)組件做一些操作,并預(yù)言產(chǎn)生的結(jié)果。如果測(cè)試結(jié)果與斷言相同則測(cè)試通過(guò)。
- 單元測(cè)試默認(rèn)測(cè)試 src 目錄下除了 main.js 之外的所有文件,可在 test/unit/index.js 文件中修改。
- Chai斷言庫(kù)中,to be been is that which and has have with at of same 這些語(yǔ)言鏈?zhǔn)菦](méi)有意義的,只是便于理解而已。
- 測(cè)試腳本由多個(gè) descibe 組成,每個(gè) describe 由多個(gè) it 組成。
- 了解異步測(cè)試
it('異步請(qǐng)求應(yīng)該返回一個(gè)對(duì)象', done => {
request
.get('https://api.github.com')
.end(function(err, res){
expect(res).to.be.an('object');
done();
});
});
了解一下 describe 的鉤子(生命周期)
describe('hooks', function() {
before(function() {
// 在本區(qū)塊的所有測(cè)試用例之前執(zhí)行
});
after(function() {
// 在本區(qū)塊的所有測(cè)試用例之后執(zhí)行
});
beforeEach(function() {
// 在本區(qū)塊的每個(gè)測(cè)試用例之前執(zhí)行
});
afterEach(function() {
// 在本區(qū)塊的每個(gè)測(cè)試用例之后執(zhí)行
});
// test cases
});
實(shí)踐
上面簡(jiǎn)單介紹了單元測(cè)試的用法,下面來(lái)動(dòng)手在Vue中進(jìn)行單元測(cè)試!
util.js
從Vue官方的demo可以看出,對(duì)于Vue的單元測(cè)試我們需要將組件實(shí)例化為一個(gè)Vue實(shí)例,有時(shí)還需要掛載到DOM上。
const Constructor = Vue.extend(Hello) // 獲得Hello組件實(shí)例 const vm = new Constructor().$mount() // 將組件掛載到DOM上
以上寫(xiě)法只是簡(jiǎn)單的獲取組件,有時(shí)候我們需要傳遞props屬性、自定義方法等,還有可能我們需要用到第三方UI框架。所以以上寫(xiě)法非常麻煩。
這里推薦Element的單元測(cè)試工具腳本Util.js,它封裝了Vue單元測(cè)試中常用的方法。下面demo也是根據(jù)該 Util.js來(lái)寫(xiě)的。
這里簡(jiǎn)單注釋了下各方法的用途。
/**
* 回收 vm,一般在每個(gè)測(cè)試腳本測(cè)試完成后執(zhí)行回收vm。
* @param {Object} vm
*/
exports.destroyVM = function (vm) {}
/**
* 創(chuàng)建一個(gè) Vue 的實(shí)例對(duì)象
* @param {Object|String} Compo - 組件配置,可直接傳 template
* @param {Boolean=false} mounted - 是否添加到 DOM 上
* @return {Object} vm
*/
exports.createVue = function (Compo, mounted = false) {}
/**
* 創(chuàng)建一個(gè)測(cè)試組件實(shí)例
* @param {Object} Compo - 組件對(duì)象
* @param {Object} propsData - props 數(shù)據(jù)
* @param {Boolean=false} mounted - 是否添加到 DOM 上
* @return {Object} vm
*/
exports.createTest = function (Compo, propsData = {}, mounted = false) {}
/**
* 觸發(fā)一個(gè)事件
* 注: 一般在觸發(fā)事件后使用 vm.$nextTick 方法確定事件觸發(fā)完成。
* mouseenter, mouseleave, mouseover, keyup, change, click 等
* @param {Element} elm - 元素
* @param {String} name - 事件名稱
* @param {*} opts - 配置項(xiàng)
*/
exports.triggerEvent = function (elm, name, ...opts) {}
/**
* 觸發(fā) “mouseup” 和 “mousedown” 事件,既觸發(fā)點(diǎn)擊事件。
* @param {Element} elm - 元素
* @param {*} opts - 配置選項(xiàng)
*/
exports.triggerClick = function (elm, ...opts) {}
示例一
示例一中我們測(cè)試了 Hello 組件的各種元素的數(shù)據(jù),學(xué)習(xí) util.js 的 destroyVM 和 createTest 方法的用法以及如何獲取目標(biāo)元素進(jìn)行測(cè)試。獲取DOM元素的方式可查看DOM 對(duì)象教程。
Hello.vue
<template>
<div class="hello">
<h1 class="hello-title">{{ msg }}</h1>
<h2 class="hello-content">{{ content }}</h2>
</div>
</template>
<script>
export default {
name: 'hello',
props: {
content: String
},
data () {
return {
msg: 'Welcome!'
}
}
}
</script>
Hello.spec.js
import { destroyVM, createTest } from '../util'
import Hello from '@/components/Hello'
describe('Hello.vue', () => {
let vm
afterEach(() => {
destroyVM(vm)
})
it('測(cè)試獲取元素內(nèi)容', () => {
vm = createTest(Hello, { content: 'Hello World' }, true)
expect(vm.$el.querySelector('.hello h1').textContent).to.equal('Welcome!')
expect(vm.$el.querySelector('.hello h2').textContent).to.have.be.equal('Hello World')
})
it('測(cè)試獲取Vue對(duì)象中數(shù)據(jù)', () => {
vm = createTest(Hello, { content: 'Hello World' }, true)
expect(vm.msg).to.equal('Welcome!')
// Chai的語(yǔ)言鏈?zhǔn)菬o(wú)意義的,可以隨便寫(xiě)。如下:
expect(vm.content).which.have.to.be.that.equal('Hello World')
})
it('測(cè)試獲取DOM中是否存在某個(gè)class', () => {
vm = createTest(Hello, { content: 'Hello World' }, true)
expect(vm.$el.classList.contains('hello')).to.be.true
const title = vm.$el.querySelector('.hello h1')
expect(title.classList.contains('hello-title')).to.be.true
const content = vm.$el.querySelector('.hello-content')
expect(content.classList.contains('hello-content')).to.be.true
})
})
輸出結(jié)果
Hello.vue
√ 測(cè)試獲取元素內(nèi)容
√ 測(cè)試獲取Vue對(duì)象中數(shù)據(jù)
√ 測(cè)試獲取DOM中是否存在某個(gè)class
示例二
示例二中我們使用 createTest 創(chuàng)建測(cè)試組件測(cè)試點(diǎn)擊事件,用 createVue 創(chuàng)建Vue示例對(duì)象測(cè)試組件 Click 的使用。這里主要可以看下到 createVue 方法的使用。
Click.vue
<template>
<div>
<span class="init-num">初始值為{{ InitNum }}</span><br>
<span class="click-num">點(diǎn)擊了{(lán){ ClickNum }}次</span><br>
<span class="result-num">最終結(jié)果為{{ ResultNum }}</span><br>
<button @click="add">累加{{ AddNum }}</button>
</div>
</template>
<script>
export default {
name: 'Click',
props: {
AddNum: {
type: Number,
default: 1
},
InitNum: {
type: Number,
default: 1
}
},
data () {
return {
ClickNum: 0,
ResultNum: 0
}
},
mounted () {
this.ResultNum = this.InitNum
},
methods: {
add () {
this.ResultNum += this.AddNum
this.ClickNum++
this.$emit('result', {
ClickNum: this.ClickNum,
ResultNum: this.ResultNum
})
}
}
}
</script>
Click.spec.js
import { destroyVM, createTest, createVue } from '../util'
import Click from '@/components/Click'
describe('click.vue', () => {
let vm
afterEach(() => {
destroyVM(vm)
})
it('測(cè)試按鈕點(diǎn)擊事件', () => {
vm = createTest(Click, {
AddNum: 10,
InitNum: 11
}, true)
let buttonElm = vm.$el.querySelector('button')
buttonElm.click()
buttonElm.click()
buttonElm.click()
// setTimeout 的原因
// 在數(shù)據(jù)改變之后,界面的變化會(huì)有一定延時(shí)。不用timeout有時(shí)候會(huì)發(fā)現(xiàn)界面沒(méi)有變化
setTimeout(done => {
expect(vm.ResultNum).to.equal(41)
expect(vm.$el.querySelector('.init-num').textContent).to.equal('初始值為11')
expect(vm.$el.querySelector('.click-num').textContent).to.equal('點(diǎn)擊了3次')
expect(vm.$el.querySelector('.result-num').textContent).to.equal('最終結(jié)果為41')
done()
}, 100)
})
it('測(cè)試創(chuàng)建Vue對(duì)象', () => {
let result
vm = createVue({
template: `
<click @click="handleClick"></click>
`,
props: {
AddNum: 10,
InitNum: 11
},
methods: {
handleClick (obj) {
result = obj
}
},
components: {
Click
}
}, true)
vm.$el.click()
vm.$nextTick(done => {
expect(result).to.be.exist
expect(result.ClickNum).to.equal(1)
expect(result.ResultNum).to.be.equal(21)
done()
})
})
輸出結(jié)果
click.vue
√ 測(cè)試按鈕點(diǎn)擊事件
√ 測(cè)試創(chuàng)建Vue對(duì)象
其他
所有示例代碼都放Github倉(cāng)庫(kù)中便于查看。如果想查看更多好的測(cè)試用例,建議配合 Util.js 看一下 Element 的單元測(cè)試腳本的寫(xiě)法,里面有很多測(cè)試腳本可以供我們學(xué)習(xí)。作為被廣大Vue用戶使用的UI組件庫(kù),測(cè)試腳本肯定也寫(xiě)很很不錯(cuò)的~甚至可以將這些腳本照抄一遍,相信這會(huì)對(duì)學(xué)習(xí)Vue組件的單元測(cè)試有很大幫助。
下面是本人看Element單元測(cè)試的筆記,供參考。
Util.js 方法包含了大多數(shù)Vue組件化的測(cè)試需求。
vm.$el vm.$nextTick 和 vm.$ref 都是在測(cè)試過(guò)程中比較常用的一些Vue語(yǔ)法糖。
需要注意: vm.$nextTick 方法是異步的,所以需要在里面使用done方法。
異步斷言,方法參數(shù)需要是 _ 或者 done
大多數(shù)時(shí)候查詢?cè)赝ㄟ^(guò) querySelector 方法查詢class獲得
vm.$el.querySelector('.el-breadcrumb').innerText
大多數(shù)情況下查詢是否存在某個(gè)Class通過(guò) classList.contains 方法獲得,查找的結(jié)果為 true 或 false
vm.$el .classList.contains('el-button--primary')
異步測(cè)試必須以 done() 方法結(jié)尾。setTimeout 和 vm.$nextTick 是常用的異步測(cè)試。
實(shí)現(xiàn)按鈕點(diǎn)擊:通過(guò)獲取按鈕元素 btn,執(zhí)行 btn.click() 方法實(shí)現(xiàn)。
由于 Vue 進(jìn)行異步更新DOM 的情況,一些依賴DOM更新結(jié)果的斷言必須在 Vue.nextTick 回調(diào)中進(jìn)行。
triggerEvent(vm.$refs.cascader.$el, 'mouseenter');
vm.$nextTick(_ => {
vm.$refs.cascader.$el.querySelector('.el-cascader__clearIcon').click();
vm.$nextTick(_ => {
expect(vm.selectedOptions.length).to.be.equal(0);
done();
});
});
Vue.js學(xué)習(xí)系列項(xiàng)目地址:https://github.com/violetjack/VueStudyDemos
以上就是本文的全部?jī)?nèi)容,希望對(duì)大家的學(xué)習(xí)有所幫助,也希望大家多多支持腳本之家。
相關(guān)文章
關(guān)于Vue?監(jiān)控?cái)?shù)組的問(wèn)題
這篇文章主要介紹了Vue?監(jiān)控?cái)?shù)組的示例,主要包括Vue?是如何追蹤數(shù)據(jù)發(fā)生變化,Vue?如何更新數(shù)組以及為什么有些數(shù)組的數(shù)據(jù)變更不能被?Vue?監(jiān)測(cè)到,對(duì)vue監(jiān)控?cái)?shù)組知識(shí)是面試比較常見(jiàn)的問(wèn)題,感興趣的朋友一起看看吧2022-05-05
vue實(shí)現(xiàn)登錄時(shí)滑塊驗(yàn)證
這篇文章主要為大家詳細(xì)介紹了vue實(shí)現(xiàn)登錄時(shí)滑塊驗(yàn)證,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2022-03-03
淺談mint-ui loadmore組件注意的問(wèn)題
下面小編就為大家?guī)?lái)一篇淺談mint-ui loadmore組件注意的問(wèn)題。小編覺(jué)得挺不錯(cuò)的,現(xiàn)在就分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2017-11-11
vue關(guān)于eslint空格縮進(jìn)等的報(bào)錯(cuò)問(wèn)題及解決
這篇文章主要介紹了vue關(guān)于eslint空格縮進(jìn)等的報(bào)錯(cuò)問(wèn)題及解決,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-05-05
基于vue實(shí)現(xiàn)web端超大數(shù)據(jù)量表格的卡頓解決
這篇文章主要介紹了基于vue實(shí)現(xiàn)web端超大數(shù)據(jù)量表格的卡頓解決,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2019-04-04
vue-cli多頁(yè)面應(yīng)用實(shí)踐之實(shí)現(xiàn)組件預(yù)覽項(xiàng)目
在最近的項(xiàng)目中遇到了一個(gè)需求,找了相關(guān)資料后終于實(shí)現(xiàn),這篇文章主要給大家介紹了關(guān)于vue-cli多頁(yè)面應(yīng)用實(shí)踐之實(shí)現(xiàn)組件預(yù)覽項(xiàng)目的相關(guān)資料,需要的朋友可以參考下2022-05-05
專業(yè)級(jí)Vue?多級(jí)菜單設(shè)計(jì)
這篇文章主要為大家介紹了專業(yè)級(jí)的Vue?多級(jí)菜單設(shè)計(jì)實(shí)現(xiàn)示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-07-07
electron+vue實(shí)現(xiàn)div contenteditable截圖功能
這篇文章主要介紹了electron+vue實(shí)現(xiàn)div contenteditable截圖功能,本文給大家介紹的非常詳細(xì),具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2020-01-01

