對於大部分前端同窗們來講,可能平時都沒怎麼接觸過單元測試🙌,頂多在初始化 Vue 項目的時候看到過它問你要不要測試,或者據說過 karma、mocha 這些名詞,但具體就不得而知了。其實這東西並不複雜,只是咱們沒去學而已,它就像 Vue 同樣容易上手,多寫個幾天,就可以像寫 Vue 同樣如魚得水了🐠。javascript
因此,咱們爲何須要單元測試呢🤔?
緣由很簡單:就是爲了減小 bug、提升產品穩定性,而不是爲了測試而測試。對咱們開發來講,它的好處也是顯而易見的:就是保證代碼質量。想一想咱們平時代碼出問題的時候,是否是經常不敢去刪除原有的代碼,而是像打補丁同樣往上加代碼,主要緣由就是沒有測試保障,你也不知道本身改了對不對、影響大不大💣。因此,若是有時間的話,單元測試仍是能夠寫寫的。css
那麼,什麼是單元測試呢?簡單來講就是對(一些不常變更的)單元進行測試,對前端來講你能夠強行理解爲😁:就是對一些通用函數和通用組件進行測試。再直白點說就是寫一些測試代碼來驗證你的源代碼是否符合預期,僅此而已。
正經點說,測試又可分爲測試驅動開發(TDD)和行爲驅動開發(BDD)兩種,什麼意思呢🤔?html
其實這些概念並不重要,咱們只要瞭解就行,畢竟這些概念也是近幾年纔出現,只是個稱呼。
那既然單元測試是個不錯的東西,爲何大部分人都不寫呢😅?說到這裏,不得不說下單元測試最大的一個缺點:就是在一開始須要花不少時間。可是在大部分狀況下,咱們不是在寫需求就是在寫需求的路上,沒時間搞它,因此就不懂。與之矛盾的是它的優勢:就是之後能夠花更少的時間😯,尤爲是若是你在開發新特性時,它能大大減小反作用。
ps: 單元測試的原則就是要儘可能獨立和單一,這樣纔有利於測試、維護和理解。固然即便用例所有經過了也要通過人工測試,由於咱們不能保證集成在一塊兒就不會有問題😬。前端
這裏先拋給你們一幅測試工具的關係圖: vue
karma 不是一個測試框架,也不是一個斷言庫,而是一個測試集成工具,它的主要做用就是集成其餘各類測試工具(支持按需配置,你能夠經過 karma 的配置文件來集成你喜歡的框架、斷言庫和瀏覽器等),而後自動打開瀏覽器運行你的測試腳本,測試結果一般會顯示在命令行中。此外它還能夠監聽測試文件的變化,而後自執行。java
mocha 是一個很經常使用的測試框架(相似的有 jasmine 和 jest 等),它既能夠在 Node 中運行,也能夠在瀏覽器中運行。它的主要做用是提供一些方便的語法來編寫測試用例,以及對用例進行分組等。一個測試腳本能夠由多個 descibe 組成,每一個 describe 又能夠由多個 it 組成。descibe 主要就是用來分組,it 就是具體的測試用例代碼。這裏簡要看下它的語法,以下:node
describe('分組一', () => {
it('測試用例描述一', () => {})
it('測試用例描述二', () => {})
})
describe('分組二', () => {
it('測試用例描述一', () => {})
it('測試用例描述二', () => {})
})
複製代碼
這個就是固定寫法,記住就行,沒有什麼爲何👀。webpack
由於 mocha 自己是不帶斷言的,因此須要和斷言庫結合使用。這裏咱們選擇 chai 這個斷言庫。它有三種不一樣風格的寫法,但意思是同樣的,就像下面這樣: git
expect(1 + 1).to.be.equal(2); // 我期待 1 + 1 等於 2
expect('hello').to.be.a('string'); // 我期待 'hello' 是個字符串
expect('').to.be.empty; // 我期待 '' 是個空值
expect({ a: 1 }).to.have.property('a'); // 我期待 { a: 1 } 有一個屬性 a
複製代碼
要注意的是 chai 斷言庫中,to be been is has have 等這些詞是沒有意義的,只是爲了讀起來比較順而已,事實上讀起來也確實順,若是你懂點基礎英語的話。github
sinon 是一個測試輔助工具,它的本質工做是測試替身,也就是用來替換測試中的部分代碼,使測試代碼變得簡潔。好比咱們要測一個函數是否被調用過,就能夠藉助 sinon.fake()
來實現,這是一個特殊的函數,如今不懂不要緊,用的時候你就知道了。
以上就是單元測試所需用到的大部分工具知識,若是你們想要加深瞭解的話,能夠自行百度。
雖然花了這麼大篇幅扯了這麼久🌚,但上面的背景知識對咱們的理解是頗有幫助的。不過,好記性不如寫代碼,下面就讓咱們趕忙擼起來吧💪。
先用 vue-cli 快速生成一個最簡版的 Vue 項目,這裏咱們選擇 default。
要安裝的依賴有點多,我就不詳細說每一個東西是幹嗎的了,裝就是了。
yarn add karma karma-chai karma-chai-spies karma-chrome-launcher karma-mocha karma-sinon-chai mocha chai sinon sinon-chai karma-webpack vue-loader -D
複製代碼
執行 ./node_modules/karma/bin/karma init
命令,一路回車,就會在根目錄生成一個 karma.conf.js 配置文件。 而後對這個文件作點修改,代碼以下:
const VueLoaderPlugin = require('vue-loader/lib/plugin')
module.exports = function(config) {
config.set({
frameworks: ['mocha', 'sinon-chai', 'chai'], // 這是配置依賴包,karma 會自動引入這些包,後續咱們就不須要 import 了
files: [
'test/**/*.test.js', // 這是要執行的測試代碼
],
preprocessors: { // 這是在測試以前要先用 webpack 處理一下
"src/**/*.*": ["webpack"],
"test/**/*.test.js": ["webpack"]
},
webpack: {
mode: 'development',
module: {
rules: [{
test: /\.js$/,
exclude: /(node_modules)/,
use: [{ loader: 'babel-loader'}]
},
{
test: /\.vue$/,
loader: 'vue-loader'
}]
},
plugins: [
new VueLoaderPlugin()
]
}
})
})
複製代碼
順便在根目錄下新建一個空的 test 目錄。
再順便在 package.json
裏面加上一個腳本命令 "test": "karma start --single-run"
。
最後的目錄結構大體以下:
ok,接下來讓咱們熱個身,寫個函數的測試用例。
在 src 目錄下新建一個 utils.js 文件,其內容以下:
// utils.js
function add(a, b) {
return a + b
}
function multiply(a, b) {
return a * b
}
export {
add,
multiply
}
複製代碼
通常來講測試文件名和源碼文件名是一致的,因此咱們在 test 目錄下新建一個 utils.test.js 文件。
import { add, multiply } from '../src/utils'
describe('工具函數測試', function() {
it('求和函數測試', function() {
let res = add(1, 1)
expect(res).to.be.equal(2)
})
it('乘法函數測試', function() {
let res = multiply(1, 1)
expect(res).to.be.equal(1)
})
})
複製代碼
嗯,就這樣,函數用例就編寫完了,固然你也能夠寫的再複雜點。
咱們直接運行 yarn test
就可以看到以下結果:
expect(res).to.be.equal(100)
,你將會獲得以下結果:
yarn test
就是執行
karma start --single-run
,karma 會根據 karma.conf.js 的配置內容來執行 test 目錄下的代碼,並自動打開瀏覽器測試,結束後又自動關閉瀏覽器(--single-run 的做用),若是有報錯就會打印在控制檯中。
接下來咱們來看看 vue 組件是怎麼測試的吧。首先,固然須要一個組件啦。
在 src 下面新建一個簡單的 demo.vue 組件,就像下面這樣:
<!-- demo.vue -->
<template>
<div class="demo" :class="isError ? 'demo--error' : ''" @click="$emit('click')">
<span class="text" :style="`opacity: ${opacity}`" :data-msg="msg">哈哈哈</span>
<slot></slot>
</div>
</template>
<script> export default { name: 'Demo', props: { msg: { type: String, default: '' }, isError: { type: Boolean, default: false }, opacity: { type: [String, Number], default: 1 } } } </script>
複製代碼
在 test 目錄下新建 demo.test.js 文件,內容以下:
import Vue from 'vue/dist/vue.common.js'
import Demo from '../src/demo.vue'
Vue.config.productionTip = false
Vue.config.devtools = false
describe('Demo 組件測試', () => {
it('存在', () => { // 首先得確保有 demo 這個東西
expect(Demo).to.exist // 不是 undefined、null、0、''等 fasly 值就是 exist
})
describe('Demo 組件的基礎功能測試', () => {
it('.text 的文本內容測試', () => {
const Constructor = Vue.extend(Demo)
const vm = new Constructor().$mount() // 實例化組件
console.log(vm.$el)
expect(vm.$el.querySelector('.text').textContent).to.equal('哈哈哈') // 我期待 .text 元素的文本內容爲 '哈哈哈'
})
})
})
複製代碼
代碼應該還算通俗易懂,其實測試用例的思路大致是一致的,主要核心思想就是:先實例化組件,而後用選擇相應元素的一些可參照的東西進行斷言,看看是否和預期相匹配。
ok,讓咱們運行 yarn test
看下效果:
equal('哈哈哈')
改錯就行,以後就再也不贅述了,就像下面這樣:
咱們直接上代碼,你們應該都能讀懂,寫法是同樣樣的😎:
// ...
describe('Demo 組件測試', () => {
describe('Demo 組件的基礎功能測試', () => {})
describe('Demo 組件的 props 測試', () => {
it('.text 的屬性值爲黃小芮', () => { // 測試標籤屬性
const Constructor = Vue.extend(Demo)
const vm = new Constructor({
propsData: { // 這是傳參的固定寫法,沒必要糾結
msg: '黃小芮'
}
}).$mount()
expect(vm.$el.querySelector('.text').getAttribute('data-msg')).to.equal('黃小芮') // 我期待 .text 元素的 data-msg 屬性值爲 '黃小芮'
})
it('.demo 是否有 demo--error 的樣式名', () => { // 測試樣式名
const Constructor = Vue.extend(Demo)
const vm = new Constructor({
propsData: {
isError: true
}
}).$mount()
expect(vm.$el.classList.contains('demo--error')).to.equal(true) // 我期待 vm.$el 的樣式列表包含 demo--error 樣式名
})
it('.text 的 opacity 樣式', () => { // 測試 css 樣式(放到頁面中才會有樣式)
const div = document.createElement('div')
document.body.appendChild(div)
const Constructor = Vue.extend(Demo)
const vm = new Constructor({
propsData: {
opacity: 0.5
}
}).$mount(div)
const ele = vm.$el.querySelector('.text')
expect(getComputedStyle(ele).opacity).to.equal('0.5') // 我期待 .text 元素的 css 樣式 opacity 值爲 '0.5',注意這裏是字符串,css 的屬性值都是字符串
})
})
})
複製代碼
這裏也直接上代碼,要注意的是 slot 和上面實例化組件的方法有點不太同樣:
// ...
describe('Demo 組件測試', () => {
describe('Demo 組件的基礎功能測試', () => {})
describe('Demo 組件的 props 測試', () => {})
describe('Demo 組件的 slot 測試', () => {
it('slot 測試', (done) => { // 異步函數須要加 done 參數說明一下,也是固定寫法
Vue.component('xr-demo', Demo)
let div = document.createElement('div')
document.body.appendChild(div)
// 這邊咱們的寫法和上面的不太同樣,不是經過 new 來實例化,而是直接寫 html
div.innerHTML = ` <xr-demo> <p id="xr"></p> </xr-demo> `
const vm = new Vue({
el: div
})
setTimeout(() => { // 這是個異步的過程,通常用 $nextTick 和 setTimeout 處理
let p = vm.$el.querySelector('#xr')
expect(p).to.exist // 咱們期待在組件中能找到 id 爲 xr 的元素
done() // 異步函數後面須要調用一下 done(),也是固定寫法
})
})
})
})
複製代碼
這裏以 click 事件爲例子🌰,那麼如何測試點擊事件呢?咱們知道點擊事件無非就是要執行一個函數,只要函數被調用了就說明點擊事件發生了,那麼怎麼證實一個函數被執行了呢🤔????嗯,是個大問題,因此,咱們須要用前面說過的 sinon.fake()
來打輔助,具體怎麼寫,仍是直接上代碼:
// ...
describe('Demo 組件測試', () => {
describe('Demo 組件的基礎功能測試', () => {})
describe('Demo 組件的 props 測試', () => {})
describe('Demo 組件的 slot 測試', () => {})
describe('Demo 組件的 event 測試', () => {
it('Demo 上的 click 事件', () => {
const Constructor = Vue.extend(Demo)
const vm = new Constructor().$mount()
const callback = sinon.fake(); // 這是 sinon 的特有函數
vm.$on('click', callback) // 添加事件監聽
vm.$el.click() // 點擊組件,會觸發上面👆那行的監聽,從而觸發 callback
expect(callback).to.have.been.called // 咱們期待 callback 被調用過
})
})
})
複製代碼
這個東西也是固定的套路,多寫就會了,就像 Vue 同樣。
假如你寫了一遍上面的那些測試用例,你會發現代碼好像有點重複,有點重複就說明咱們能夠優化它,因而就要說到 mocha 的幾個鉤子函數(這裏只大概描述一下):
describe('hooks', function() {
before(function() {
// runs before all tests in this block
});
after(function() {
// runs after all tests in this block
});
beforeEach(function() {
// runs before each test in this block
});
afterEach(function() {
// runs after each test in this block
});
// test cases
it('case one', () => {})
it('case two', () => {})
});
複製代碼
也就是說咱們在執行 it 以前會先調用 beforeEach 這個鉤子,執行 it 以後調用 afterEach 這個鉤子。這樣一來咱們就能夠把實例化組件的代碼抽離出來寫在 beforeEach 裏面。
另外,你可能還注意到,咱們的實例沒有及時銷燬,因此咱們也能夠在 afterEach 這個鉤子裏面作相應的處理,就像下面這樣:
afterEach(function() {
// 移除元素並釋放內存
vm.$el.remove()
vm.$destroy()
});
複製代碼
咱們可不能夠保存的時候就自動執行 yarn test
呢。嗯,是能夠的,小小修改一下最初的腳本命令就行,就像這樣:"test": "karma start"
,這下咱們保存的時候它就會自動測試一遍了。
👌,接下來就是見證奇蹟的時刻😊。如今讓咱們打開 Element 的源碼來看看別人的單元測試是怎麼寫的(瞟一眼就行):
因此,最終咱們要怎麼應用到實際工做中呢?em...我想大部分公司的後臺管理系統應該是一個施展才華的好地方。至於覆蓋率,多寫多覆蓋羅,對於大部分前端同窗來講沒必要太較真,畢竟咱們是要寫需求的啊。最後的最後,其實不少東西都不難,只是咱們沒碰觸過因此總以爲高不可攀。常言道會者不難,難者不會,說的就是這個道理(大讚無疆👍👍👍。。。)。
ps: 若有須要上述代碼的請點擊這裏: 單元測試 demo 傳送門 ps: 後面我會每個月寫篇【大白話】系列文章,用通俗的語言讓你看了就懂,歡迎關注。