Vue單元測試Karma+Mocha
Karma是一個基於Node.js的JavaScript測試執行過程管理工具(Test Runner)。該工具在Vue中的主要作用是將項目運行在各種主流Web瀏覽器進行測試。
換句話說,它是一個測試工具,能讓你的代碼在瀏覽器環境下測試。需要它的原因在於,你的代碼可能是設計在瀏覽器端執行的,在node環境下測試可能有些bug暴露不出來;另外,瀏覽器有兼容問題,karma提供了手段讓你的代碼自動在多個瀏覽器(chrome,firefox,ie等)環境下運行。如果你的代碼只會運行在node端,那么你不需要用karma。
Mocha是一個測試框架,在vue-cli中配合chai斷言庫實現單元測試。
Mocha的常用命令和用法不算太多,看阮一峰老師的測試框架 Mocha 實例教程就可以大致了解了。
而Chai斷言庫可以看Chai.js斷言庫API中文文檔,很簡單,多查多用就能很快掌握。
對測試框架的理解
npm run unit 執行過程
執行 npm run unit 命令
開啟Karma運行環境
使用Mocha去逐個測試用Chai斷言寫的測試用例
在終端顯示測試結果
如果測試成功,karma-coverage 會在 ./test/unit/coverage 文件夾中生成測試覆蓋率結果的網頁。
Karma的Vue的karma配置,簡單注釋了下:
var webpackConfig = require('../../build/webpack.test.conf') module.exports = function (config) { config.set({ // 瀏覽器 browsers: ['PhantomJS'], // 測試框架 frameworks: ['mocha', 'sinon-chai', 'phantomjs-shim'], // 測試報告 reporters: ['spec', 'coverage'], // 測試入口文件 files: ['./index.js'], // 預處理器 karma-webpack preprocessors: { './index.js': ['webpack', 'sourcemap'] }, // Webpack配置 webpack: webpackConfig, // Webpack中間件 webpackMiddleware: { noInfo: true }, // 測試覆蓋率報告 // https://github.com/karma-runner/karma-coverage/blob/master/docs/configuration.md coverageReporter: { dir: './coverage', reporters: [ { type: 'lcov', subdir: '.' }, { type: 'text-summary' } ] } }) }
Mocha和chai
import Vue from 'vue' // 導入Vue用於生成Vue實例
import Hello from '@/components/Hello' // 導入組件
// 測試腳本里面應該包括一個或多個describe塊,稱為測試套件(test suite)
describe('Hello.vue', () => {
// 每個describe塊應該包括一個或多個it塊,稱為測試用例(test case)
it('should render correct contents', () => {
const Constructor = Vue.extend(Hello) // 獲得Hello組件實例
const vm = new Constructor().$mount() // 將組件掛在到DOM上
//斷言:DOM中class為hello的元素中的h1元素的文本內容為Welcome to Your Vue.js App
expect(vm.$el.querySelector('.hello h1').textContent)
.to.equal('Welcome to Your Vue.js App')
})
})
需要知道的知識點:
* 測試腳本都要放在 test/unit/specs/ 目錄下。
* 腳本命名方式為 [組件名].spec.js。
* 所謂斷言,就是對組件做一些操作,並預言產生的結果。如果測試結果與斷言相同則測試通過。
* 單元測試默認測試 src 目錄下除了 main.js 之外的所有文件,可在 test/unit/index.js 文件中修改。
* Chai斷言庫中,to be been is that which and has have with at of same 這些語言鏈是沒有意義的,只是便於理解而已。
* 測試腳本由多個 descibe 組成,每個 describe 由多個 it 組成。
* 了解異步測試
it('異步請求應該返回一個對象', done => { request .get('https://api.github.com') .end(function(err, res){ expect(res).to.be.an('object'); done(); }); }); 了解一下 describe 的鈎子(生命周期) describe('hooks', function() { before(function() { // 在本區塊的所有測試用例之前執行 }); after(function() { // 在本區塊的所有測試用例之后執行 }); beforeEach(function() { // 在本區塊的每個測試用例之前執行 }); afterEach(function() { // 在本區塊的每個測試用例之后執行 }); // test cases });
實例:
util.js
從Vue官方的demo可以看出,對於Vue的單元測試需要將組件實例化為一個Vue實例,有時還需要掛載到DOM上。
const Constructor = Vue.extend(Hello) // 獲得Hello組件實例
const vm = new Constructor().$mount() // 將組件掛載到DOM上
以上寫法只是簡單的獲取組件,有時候我們需要傳遞props屬性、自定義方法等,還有可能我們需要用到第三方UI框架。
推薦Element的單元測試工具腳本Util.js,它封裝了Vue單元測試中常用的方法。下面demo也是根據該 Util.js來寫的。
這里簡單注釋了下各方法的用途。
/**
* 回收 vm,一般在每個測試腳本測試完成后執行回收vm。
* @param {Object} vm
*/
exports.destroyVM = function (vm) {}
/**
* 創建一個 Vue 的實例對象
* @param {Object|String} Compo - 組件配置,可直接傳 template
* @param {Boolean=false} mounted - 是否添加到 DOM 上
* @return {Object} vm
*/
exports.createVue = function (Compo, mounted = false) {}
/**
* 創建一個測試組件實例
* @param {Object} Compo - 組件對象
* @param {Object} propsData - props 數據
* @param {Boolean=false} mounted - 是否添加到 DOM 上
* @return {Object} vm
*/
exports.createTest = function (Compo, propsData = {}, mounted = false) {}
/**
* 觸發一個事件
* 注: 一般在觸發事件后使用 vm.$nextTick 方法確定事件觸發完成。
* mouseenter, mouseleave, mouseover, keyup, change, click 等
* @param {Element} elm - 元素
* @param {String} name - 事件名稱
* @param {*} opts - 配置項
*/
exports.triggerEvent = function (elm, name, ...opts) {}
/**
* 觸發 “mouseup” 和 “mousedown” 事件,既觸發點擊事件。
* @param {Element} elm - 元素
* @param {*} opts - 配置選項
*/
exports.triggerClick = function (elm, ...opts) {}
示例一
示例一中測試了Hello組件的各種元素的數據,學習 util.js 的 destroyVM 和 createTest 方法的用法以及如何獲取目標元素進行測試。獲取DOM元素的方式可查看DOM 對象教程。
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('測試獲取元素內容', () => { 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('測試獲取Vue對象中數據', () => { vm = createTest(Hello, { content: 'Hello World' }, true) expect(vm.msg).to.equal('Welcome!') // Chai的語言鏈是無意義的,可以隨便寫。如下: expect(vm.content).which.have.to.be.that.equal('Hello World') }) it('測試獲取DOM中是否存在某個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 }) })
Hello.vue
測試獲取元素內容
測試獲取Vue對象中數據
測試獲取DOM中是否存在某個class
示例二中我們使用 createTest 創建測試組件測試點擊事件,用 createVue 創建Vue示例對象測試組件 Click 的使用。這里主要可以看下到 createVue 方法的使用。
Click.vue <template> <div> <span class="init-num">初始值為{{ InitNum }}</span><br> <span class="click-num">點擊了{{ ClickNum }}次</span><br> <span class="result-num">最終結果為{{ 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('測試按鈕點擊事件', () => { vm = createTest(Click, { AddNum: 10, InitNum: 11 }, true) let buttonElm = vm.$el.querySelector('button') buttonElm.click() buttonElm.click() buttonElm.click() // setTimeout 的原因 // 在數據改變之后,界面的變化會有一定延時。不用timeout有時候會發現界面沒有變化 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('點擊了3次') expect(vm.$el.querySelector('.result-num').textContent).to.equal('最終結果為41') done() }, 100) }) it('測試創建Vue對象', () => { 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() }) })
輸出結果
click.vue
測試按鈕點擊事件
測試創建Vue對象
其他
所有示例代碼都放Github倉庫中便於查看。如果想查看更多好的測試用例,建議配合 Util.js
Element的單元測試腳本的寫法,里面有很多測試腳本可以學習
作為被廣大Vue用戶使用的UI組件庫,測試腳本也寫很不錯,對學習Vue組件的單元測試有很大幫助。
------------
Util.js 方法包含了大多數Vue組件化的測試需求。
vm.$el vm.$nextTick 和 vm.$ref 都是在測試過程中比較常用的一些Vue語法糖。
需要注意: vm.$nextTick 方法是異步的,所以需要在里面使用done方法。
異步斷言,方法參數需要是 _ 或者 done
大多數時候查詢元素通過 querySelector 方法查詢class獲得
vm.$el.querySelector(‘.el-breadcrumb’).innerText
大多數情況下查詢是否存在某個Class通過 classList.contains 方法獲得,查找的結果為 true 或 false
vm.$el .classList.contains(‘el-button–primary’)
異步測試必須以 done() 方法結尾。setTimeout 和 vm.$nextTick 是常用的異步測試。
實現按鈕點擊:通過獲取按鈕元素 btn,執行 btn.click() 方法實現。
由於 Vue 進行 異步更新DOM 的情況,一些依賴DOM更新結果的斷言必須在 Vue.nextTick 回調中進行。
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(); }); });