做爲一個以 文檔豐富 而廣爲人知的前端開發框架, Vue.js 的官方文檔中分別在《教程-工具-單元測試》、《Cookbook-Vue組件的單元測試》裏對 Vue 組件的單元測試方法作出了介紹,並提供了官方的單元測試實用工具庫 Vue Test Utils;甚至在狀態管理工具 Vuex 的文檔裏也不忘留出《測試》一章。javascript
那是什麼緣由讓 Vue.js 的開發團隊如此重視單元測試,要在這個一樣以 易於上手 爲賣點的框架中大力科普呢?html
官方文檔中給出了很是清楚的說法:前端
組件的單元測試有不少好處:
- 提供描述組件行爲的文檔
- 節省手動測試的時間
- 減小研發新特性時產生的 bug
- 改進設計
- 促進重構
自動化測試使得大團隊中的開發者能夠維護複雜的基礎代碼。
複製代碼
本文做爲《對 React 組件進行單元測試》一文的姊妹篇,將照貓畫虎式的嘗試面對初學和向中級進階的開發者,對單元測試在 Vue.js 技術棧 中的應用作出入門介紹。vue
單元測試(unit testing),是指對軟件中的最小可測試單元進行檢查和驗證。java
簡單來講,單元
就是人爲規定的最小的被測功能模塊。單元測試是在軟件開發過程當中要進行的最低級別的測試活動,軟件的獨立單元將在與程序的其餘部分相隔離的狀況下進行測試。git
對於開發活動中的各類測試,上圖是一種最多見的劃分方法:從下至上依次爲 單元測試->集成測試->端到端測試 ,隨着其集成度的遞增,對應的自動化程度遞減。ajax
端到端(在瀏覽器等真實場景中走通功能而把程序當成黑盒子的測試)與集成測試(集合多個測試過的單元一塊兒測試)的反饋與修復的週期比較長、運行速度慢,測試運行不穩定,因爲不少時候還要靠人工手動進行,維護成本也很高。而單元測試只針對具體一個方法或API,定位準確,採用 mock 機制,運行速度很是快(毫秒級),又是開發人員在本地執行,反饋修復及時,成本較低。vuex
咱們把絕大部分能在單元測試裏覆蓋的用例都放在單元測試覆蓋,只有單元測試測不了的,纔會經過端到端與集成測試來覆蓋。npm
好比咱們有這樣一個模塊,暴露兩個方法用以對菜單路徑進行一些處理:json
// src/menuChecker.js
export function getRoutePath(str) {
let to = ""
//...
return to;
}
export function getHighlight(str) {
let hl = "";
//...
return hl;
}
複製代碼
編寫對應的測試文件:
import {
getRoutePath,
getHighlight
} from "@/menuChecker";
describe("檢查菜單路徑相關函數", ()=>{
it("應該得到正確高亮值", ()=>{
expect( getHighlight("/myworksheet/(.*)") ).toBe("myTickets");
});
it("應該爲未知路徑取得默認的高亮值", ()=>{
expect( getHighlight("/myworksheet/ccc/aaa") ).toBe("mydefaulthl111");
});
it("應該補齊開頭的斜槓", ()=>{
expect( getRoutePath("/worksheet/list") ).toBe('/worksheet/list');
});
it("應該能修正非法的路徑", ()=>{
expect( getRoutePath("/myworksheet/(.*)") ).toBe("/myworksheet/list");
});
});
複製代碼
運行該測試文件,獲得以下輸出:
運行結果能夠說很是友好了,雖然醒目的提示了 FAIL,可是哪條判斷錯了、錯在哪一行、實際的返回值與預期的區別,甚至代碼覆蓋率的表格,都分別展現了出來;尤爲是最重要的對錯結果,分別用綠色紅色加以展現。
真相只有一個,要麼是目標模塊寫的有問題,要麼是測試條件寫錯了 -- 總之咱們對其修正後從新運行:
由此,咱們對一次單元測試的過程有了基本的瞭解。
首先,對所謂「單元」的定義是靈活的,能夠是一個函數,能夠是一個模塊,也能夠是一個 Vue Component。
其次,因爲測試結果中,成功的用例會用綠色表示,而失敗的部分會顯示爲紅色,因此單元測試也經常被稱爲 「Red/Green Testing」 或 「Red/Green Refactoring」,其通常步驟能夠概括爲:
測試框架的做用是提供一些方便的語法來描述測試用例,以及對用例進行分組。
斷言是單元測試框架中核心的部分,斷言失敗會致使測試不經過,或報告錯誤信息。
對於常見的斷言,舉一些例子以下:
同等性斷言 Equality Asserts
比較性斷言 Comparison Asserts
類型性斷言 Type Asserts
條件性測試 Condition Test
斷言庫主要提供上述斷言的語義化方法,用於對參與測試的值作各類各樣的判斷。這些語義化方法會返回測試的結果,要麼成功、要麼失敗。常見的斷言庫有 Should.js, Chai.js 等。
爲某個特殊目標而編制的一組測試輸入、執行條件以及預期結果,以便測試某個程序路徑或覈實是否知足某個特定需求。
通常的形式爲:
it('should ...', function() {
...
expect(sth).toEqual(sth);
});
複製代碼
一般把一組相關的測試稱爲一個測試套件
通常的形式爲:
describe('test ...', function() {
it('should ...', function() { ... });
it('should ...', function() { ... });
...
});
複製代碼
正如
spy
字面的意思同樣,咱們用這種「間諜」來「監視」函數的調用狀況
經過對監視的函數進行包裝,能夠經過它清楚的知道該函數被調用過幾回、傳入什麼參數、返回什麼結果,甚至是拋出的異常狀況。
var spy = sinon.spy(MyComp.prototype, 'someMethod');
...
expect(spy.callCount).toEqual(1);
複製代碼
有時候會使用
stub
來嵌入或者直接替換掉一些代碼,來達到隔離的目的
一個stub
可使用最少的依賴方法來模擬該單元測試。好比一個方法可能依賴另外一個方法的執行,然後者對咱們來講是透明的。好的作法是使用stub 對它進行隔離替換。這樣就實現了更準確的單元測試。
var myObj = {
prop: function() {
return 'foo';
}
};
sinon.stub(myObj, 'prop').callsFake(function() {
return 'bar';
});
myObj.prop(); // 'bar'
複製代碼
mock
通常指在測試過程當中,對於某些不容易構造或者不容易獲取的對象,用一個虛擬的對象來建立以便測試的測試方法
廣義的講,以上的 spy 和 stub 等,以及一些對模塊的模擬,對 ajax 返回值的模擬、對 timer 的模擬,都叫作 mock 。
用於統計測試用例對代碼的測試狀況,生成相應的報表,好比 istanbul
是常見的測試覆蓋率統計工具。
istanbul
也就是土耳其首都 「伊斯坦布爾」,這樣命名是由於土耳其地毯世界聞名,而地毯是用來"覆蓋"的😷。
回顧一下上面的圖:
表格中的第2列至第5列,分別對應了四個衡量維度:
if
代碼塊都執行了測試結果根據覆蓋率被分爲「綠色、黃色、紅色」三種,應該關注這些指標,測試越全面,就能提供更高的保證。
同時也沒有必要一味追求行覆蓋率,由於它會致使咱們過度關注組件的內部實現細節,從而致使瑣碎的測試。
不一樣於"傳統的"(其實也沒出現幾年)的 jasmine / Mocha / Chai 等前端測試框架;Jest
的使用更簡單(也許就是這個單詞的本意「俏皮話、玩笑話」的意思),而且提供了更高的集成度、更豐富的功能。
Jest 是一個由 Facebook 開發的測試運行器,相對其餘測試框架,其特色就是就是內置了經常使用的測試工具,好比自帶斷言、測試覆蓋率工具,實現了開箱即用。
此外, Jest 的測試用例是並行執行的,並且只執行發生改變的文件所對應的測試,提高了測試速度。
Jest 號稱本身是一個 「Zero configuration testing platform」,只需在 npm scripts
裏面配置了test: jest
,便可運行npm test
,自動識別並測試符合其規則的( Vue.js 項目中通常是 __tests__
目錄下的)用例文件。
實際使用中,適當的在 package.json 的 jest 字段或獨立的 jest.config.js 裏自定義配置一下,會獲得更適合咱們的測試場景。
參考文檔 vue-test-utils.vuejs.org/zh/guides/t… ,能夠很快在 Vue.js 項目中配置好 Jest 測試環境。
編寫單元測試的語法一般很是簡單;對於jest
來講,因爲其內部使用了 Jasmine 2
來進行測試,故其用例語法與 Jasmine 相同。
實際上,只要先記這住四個單詞,就足以應付大多數測試狀況了:
describe
: 定義一個測試套件it
:定義一個測試用例expect
:斷言的判斷條件toEqual
:斷言的比較結果describe('test ...', function() {
it('should ...', function() {
expect(sth).toEqual(sth);
expect(sth.length).toEqual(1);
expect(sth > oth).toEqual(true);
});
});
複製代碼
圖中這位「我牽着馬」的並非捲簾大將沙悟淨...其實圖中的故事正是人所皆知的「特洛伊木馬」;大概意思就是希臘人圍困了特洛伊人十多年,久攻不下,心生一計,把營盤都撤了,只留下一個巨大的木馬(裏面裝着士兵),以及這位被扒光還被打得夠嗆的人,也就是此處要談的主角 sinon,由他欺騙特洛伊人 --- 後面的劇情你們就都熟悉了。
因此這個命名的測試工具呢,也正是各類假裝滲透方法的合集,爲單元測試提供了獨立而豐富的 spy, stub 和 mock 方法,兼容各類測試框架。
雖然 Jest 自己也有一些實現 spy 等的手段,但 sinon 使用起來更加方便。
Vue Test Utils 是 Vue.js 官方的單元測試實用工具庫;該工具庫使用起來和用以測試 React 組件的 Enzyme 工具庫很是類似
它模擬了一部分相似 jQuery 的 API,很是直觀而且易於使用和學習,提供了一些接口和幾個方法來減小測試的樣板代碼,方便判斷、操縱和遍歷 Vue Component 的輸出,而且減小了測試代碼和實現代碼之間的耦合。
通常使用其 mount()
或 shallowMount()
方法,將目標組件轉化爲一個 Wrapper
對象,並在測試中調用其各類方法,例如:
import { mount } from '@vue/test-utils'
import Foo from './Foo.vue'
describe('Foo', () => {
it('renders a div', () => {
const wrapper = mount(Foo)
expect(wrapper.contains('div')).toBe(true)
})
})
複製代碼
import { shallowMount } from "@vue/test-utils";
import Vue from 'vue';
import VueI18n from 'vue-i18n';
import i18nMessage from '@/i18n';
import Comp from "@/components/Device.vue";
const fakeData = { //假數據
deviceNo: "abcdefg",
deviceSpace: 45,
deviceStatus: 2,
devices: [
{
id: "test001",
location: "12",
status: 1
},
{
id: "test002",
location: "58",
status: 3
},
{
id: "test003",
location: "199",
status: 4
}
]
};
Vue.use(VueI18n); //重現必要的依賴
const i18n = new VueI18n({
locale: 'zh-CN',
silentTranslationWarn: true,
missing: (locale, key, vm) => key,
messages: i18nMessage
});
let wrapper = null;
const makeWrapper = ()=>{
wrapper = shallowMount( Comp, {
i18n, //看這裏
propsData: { //還有這裏
unitHeight: 5,
data: fakeData
}
} );
};
afterEach(()=>{ //也很常見的用法
if (!wrapper) return;
wrapper = null;
});
describe("test Device.vue", ()=>{
it("should be a VUE instance", ()=>{
makeWrapper();
expect( wrapper.isVueInstance() ).toBeTruthy();
});
it("應該有正常的總高度", ()=>{
makeWrapper();
expect( wrapper.vm.totalHeight ).toBe( 1230 );
});
it("應該渲染正確的設備數量", ()=>{
makeWrapper();
expect( wrapper.findAll('.deviceitem').length ).toBe( 3 );
});
it("指定的設備應該在正確的位置", ()=>{
makeWrapper();
const sty = wrapper.findAll('.deviceitem').at(1).attributes('style');
expect( sty ).toMatch( /height\:\s*20px/ );
expect( sty ).toMatch( /bottom\:\s*20px/ );
});
it("應該渲染正確的tooltip", ()=>{
makeWrapper();
//這裏的用法值得注意
const popper_ref = wrapper.find({ref: 'device_tooltip_test002'});
expect( popper_ref.exists() ).toBeTruthy();
const cont = popper_ref.find('.tooltip_cont');
expect( cont.html() ).toMatch(/所在位置\:\s58/);
});
it("應該渲染正確的設備分類", ()=>{
makeWrapper();
const badge = wrapper.find('.badge');
expect( badge.exists() ).toBeTruthy();
expect( badge.findAll('li').length ).toBe(4);
expect( badge.findAll('li').at(2).text() ).toBe('噴霧設備');
});
it("當點擊了關閉按鈕,應該再也不顯示", (done)=>{ //異步的用例
makeWrapper();
wrapper.vm.$nextTick(()=>{ //再看這裏
expect(
wrapper.find('.devices_container').exists()
).toBeFalsy();
done();
});
});
});
複製代碼
這裏無需逐條的解釋,主要的 API 在 Jest
和 Vue Test Utils
的文檔裏都能找到。
其中值得注意的小經驗,一是一些異步更新(好比代碼中有延時)後正確使用 wrapper.vm.$nextTick
;二是對於一些掛載到 document.body 等外部位置的組件元素,要靠 wrapper.find({ref: xxx})
取得其引用。
寫好的單元測試,若是僅僅要靠每次 npm test
手動執行,必然會有日久忘記、逐漸過期,最後甚至沒法執行的狀況。
有多個時間點能夠做爲選擇,插入自動執行單元測試 -- 例如每次保存文件、每次執行 build 等;此處咱們選擇了一種很簡單的配置辦法:
首先在項目中安裝 pre-commit
依賴包;而後在 package.json
中配置 npm scripts :
"scripts": {
...
"test": "jest"
},
"pre-commit": [
"test"
],
複製代碼
這樣在每次 git commit
以前,項目中存在的單元測試就會自動執行一次,每每就避免了 「改一個 bug,送十個新 bug」 的窘況。
單元測試除了減小錯誤,另外一個顯著的好處是能讓咱們組件化的思路愈來愈清晰,養成日益良好的習慣。
一個被驗證過針對給定的輸入會渲染出符合指望的輸出的組件,稱爲 測試經過的 組件;
一個 可測試的(testable) 組件意味着其易於測試
如何確保一個組件如指望的工做呢?
咱們可能習慣於依靠雙手和眼睛,一次次的驗證咱們寫過的組件;但若是你打算對每一個組件的每一個改動都手動驗證的話,或早或晚就會由於疲憊或懈怠,致使瑕疵留在代碼中。
這就是自動化的單元測試爲什麼重要的緣由。單元測試保證了每次對組件作出的更改後,組件都能正確工做。
單元測試並不僅與早期發現 bug 有關。另外一個重要的方面是用其檢驗組件架構化水平優劣的能力。
一個 沒法測試 或 難以測試 的組件,基本上就等同於 設計得很拙劣 的組件.
組件之因此難以測試,是由於其有太多的 props、依賴、引用的模型和對全局變量的訪問 -- 這都是不良設計的標誌。
一個設計不佳的組件,就會變成沒法測試的,進而你就會簡單的跳過單元測試,又致使了其保持未測試狀態,變成一個惡性循環。
假設要對 NumStepper.vue 組件進行測試
//NumStepper.vue
<template>
<div>
<button class="plus" v-on:click="updateNumber(+1)">加</button>
<button class="minus" v-on:click="updateNumber(-1)">減</button>
<button class="zero" v-on:click="clear">清</button>
</div>
</template>
<script>
export default {
props: {
targetData: Object,
clear: Function
},
methods: {
updateNumber: function(n) {
this.targetData.num += n;
}
}
}
</script>
複製代碼
該組件又依賴一個外層組件給其提供數據和方法:
//NumberDisplay.vue
<template>
<div>
<p>{{somedata.num}}</p>
<NumStepper :targetData="somedata" :clear="clear" />
</div>
</template>
<script>
import NumStepper from "./NumStepper"
export default {
components: {
NumStepper
},
data() {
return {
somedata: {
num: 999
},
tgt: this
}
},
methods: {
clear: function() {
this.somedata.num = 0;
}
}
}
</script>
複製代碼
這樣一來,咱們的測試就得這樣寫:
import { shallowMount } from "@vue/test-utils";
import Vue from 'vue';
import NumStepper from '@/components/NumStepper';
import NumberDisplay from '@/components/NumberDisplay';
describe("測試 NumStepper 組件", ()=>{
it("應該可以影響外層組件的數據", ()=>{
const display = shallowMount(NumberDisplay);
const wrapper = shallowMount(NumStepper, {
propsData: {
targetData: display.vm.somedata,
clear: display.vm.clear
}
});
expect(display.vm.somedata.num).toBe(999);
wrapper.find('.plus').trigger('click');
wrapper.find('.plus').trigger('click');
expect(display.vm.somedata.num).toBe(1001);
wrapper.find('.minus').trigger('click');
expect(display.vm.somedata.num).toBe(1000);
wrapper.find('.zero').trigger('click');
expect(display.vm.somedata.num).toBe(0);
})
});
複製代碼
<NumStepper>
測試起來很是複雜,由於它關聯了外部組件的實現細節。
測試場景中須要一個額外的 <NumberDisplay>
組件,用來重現外部組件、向目標組件傳遞數據和方法,並檢驗目標組件是否正確修改了外部組件的狀態。
不難想象,假如 <NumberDisplay>
組件再依賴其餘組件或環境變量、全局方法等,事情將變得更糟糕,可能須要單獨實現若干測試專用組件,甚至根本沒法測試。
當 <NumStepper>
獨立於外部組件的細節時,測試就簡單了。讓咱們實現並測試一下合理封裝版本的 <NumStepper>
組件:
//NumStepper2.vue
<template>
<div>
<button class="plus" v-on:click="updateFunc(+1)">加</button>
<button class="minus" v-on:click="updateFunc(-1)">減</button>
<button class="zero" v-on:click="clearFunc">清</button>
</div>
</template>
<script>
export default {
props: {
updateFunc: Function,
clearFunc: Function
}
}
</script>
複製代碼
在測試中,就不用引入額外的組件了:
import { shallowMount } from "@vue/test-utils";
import Vue from 'vue';
import NumStepper from '@/components/NumStepper2';
describe("測試 NumStepper 組件", ()=>{
it("應該可以影響外層組件的數據", ()=>{
const obj = {
func1: function(){},
func2: function(){}
};
const spy1 = jest.spyOn(obj, "func1");
const spy2 = jest.spyOn(obj, "func2");
const wrapper = shallowMount(NumStepper, {
propsData: {
updateFunc: spy1,
clearFunc: spy2
}
});
wrapper.find('.plus').trigger('click');
expect(spy1).toHaveBeenCalled();
wrapper.find('.minus').trigger('click');
expect(spy1).toHaveBeenCalled();
wrapper.find('.zero').trigger('click');
expect(spy2).toHaveBeenCalled();
})
});
複製代碼
注:該示例中只是檢驗了是否被點擊,還能夠引入 sinon 的相關方法檢驗傳入的參數等,寫出更完備的測試。
單元測試做爲一種經典的開發和重構手段,在軟件開發領域被普遍承認和採用;前端領域也逐漸積累起了豐富的測試框架和方法。
單元測試能夠爲咱們的開發和維護提供基礎保障,使咱們在思路清晰、心中有底的狀況下完成對代碼的搭建和重構。
封裝好則測試易,反之不恰當的封裝讓測試變得困難。
可測試性是一個檢驗組件結構良好程度的實踐標準。