
先來幾個專業詞匯,這樣顯得高大上一點(不存在的=。=)
BDD: Behavior-Driven Development (行為驅動開發)TDD: Test-Driven Development (測試驅動開發)ATDD: Acceptance Test Driven Development(驗收測試驅動開發)
好,說完了,然后我們廢話不多說,直接進入正題。我會從多個測試框架入手,結合各種斷言庫,用代碼方式說明。
單元測試(Unit Testing),是指對軟件中的最小可測試單元進行檢查和驗證。
當今所有著名的框架都要進行單元測試,經過測試的框架,它的信任度顯然高于未測試的框架。
這里,我們介紹一下karma這個前端的單元測試框架。

首先我們來安裝一波:
新建一個空文件夾,然后在空文件夾中打開終端輸入
npm init -y
(sudo) npm install karma-cli -g
npm install karma karma-jasmine karma-chrome-launcher jasmine-core --save-dev
npm install karma-phantomjs-launcher --save-dev

你安裝karma-cli這個倒是說得過去,可是這個jasmine是啥,這個chrome-launcher和phantomjs-launcher又是啥?
沒錯,單說測試框架是不完整的,必須要有斷言庫與之相配合,這里的jasmine就是斷言庫。

啥是斷言(assert)?
根據概念:
斷言是編程術語,表示為一些布爾表達式,程序員相信在程序中的某個特定點該表達式值為真,可以在任何時候啟用和禁用斷言驗證,因此可以在測試時啟用斷言而在部署時禁用斷言。
一言以蔽之,老子/老娘說啥就是啥!聽起來好像挺霸道的。那么具體呢?
順著karma的正常流程向下走,我們來寫一個簡單的單元測試。在終端輸入:
karma init
你會發現,需要做一個調查問卷了,問題如下:
> 請問你要用哪種測試框架呢?
> 按tab鍵選擇,按回車鍵進入下一個問題。
> jasmine
(因為我們安裝的是jasmine,選什么斷言庫都別忘了安裝一下)> 您想要使用Require.js么?
> 選擇yes的話,會安裝Require.js插件。
> 按tab鍵選擇,按回車鍵進入下一個問題。
> no
(這里我們選擇no)> 你想要在什么瀏覽器中測試呢?
> 按tab鍵選擇,輸入空字符串進入下一個問題。
> Chrome
> PhantomJS
>注:上面的選擇這兩個瀏覽器的原因是我們之前安裝了這兩個瀏覽器的啟動器(launcher)> 需要測試的源文件和測試命令文件放在哪呢?
你可以使用通配符(glob patterns)來匹配文件,比如:"js/*.js" 或 "test/**/*Spec.js"
輸入空字符串進入下一個問題。
>
(這里先留空,可根據測試情況靈活配置)>在符合匹配的文件中有哪些文件可以排除在外呢?
你可以使用通配符來匹配文件,比如:"**/*.swp"
輸入空字符串進入下一個問題。
> > 你想要Karma根據文件的變化立即做出響應么?
> yes
之后,你就會發現你的文件夾里多了一個文件:

打開這個文件,你會發現里面是一個配置項函數:
module.exports = function(config) {basePath: '', // 根路徑將會同files和excluede項中的相對路徑相關聯frameworks: ['jasmine'], // 所使用的測試框架files: [], // 這里是需要測試的文件列表,有多種配置方式exclude: [], // 測試過程中排除在外的文件列表reporters: ['progress'], // 測試結果的匯報方式,port: 9876, // web服務器接口colors: true, // 是否使用彩色報告logLevel: config.LOG_INFO, // 日志級別,可配置的值有: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUGautoWatch: true, // 是否自動觀測文檔改變并執行測試命令browsers: ["Chrome", "PhantomJS"], // 用哪些瀏覽器測試呢singleRun: false, // 持續集成模式,如果設置成true,Karma將自行捕獲瀏覽器,運行測試并根據結果退出,concurrency: Infinity // 并發數,同時跑多少個瀏覽器進行測試,默認無上限
}
默認會生成的配置項就是上面這些,更完整的配置請點我
這里稍微提一下browsers配置項,它可以配置高達8種瀏覽器:

每一種都需要安裝對應的launcher。其中有兩個需要注意chromeHeadless和PhantomJS。這兩個是無頭瀏覽器。所謂無頭瀏覽器就是沒有腦袋的瀏覽器。

無頭瀏覽器即headless browser,是一種沒有界面的瀏覽器。既然是瀏覽器那么瀏覽器該有的東西它都應該有,只是看不到界面而已。因此這種瀏覽器沒有渲染UI的過程,用于測試時的速度很快。
這就回答了上文launcher是啥的問題。畢竟,沒有瀏覽器靠腦補可沒法測試啊(真實)
言歸正傳。我們回到karma測試本身。接下來,我們修改一下配置:
files: ["src/srcTest/**/*.js", "test/unit/**/*.js"]
注意,上述寫法只是配置寫法中的一種, 配置的文件位置也是隨您自己指定,更詳細的配置請點我
采用上文寫法的話,我們在files數組里面配置的第一項是需要測試的文件,第二項就是用什么方法去測試它的文件。
因此,我們也在文件里創建對應的文件夾:

這里有一個要注意的點。我們的需要測試的文件和測試驅動文件的名字是一一對應的,區別就在于測試驅動文件的名字后要加上.spec
那么我們就在srcTest的文件里面寫點什么吧....
newBee.js
// 減法函數
function minus(x) {return function(y) {return x - y;};
}
testKarma.js
// 加法函數
function add(x) {return function(y) {return x + y;};
}// 乘法函數
function multi(x) {return function(y) {return x * y;};
}//if函數測試
function ifTest(boolean) {if (boolean) {return "熱熱";} else {return "涼涼";}
}// 反轉字符串
function reverseStr (string) {return string.split("").reverse().join("");
}

那么接下來,就在.spec文件里寫入對應的測試斷言。我滴個龜龜,終于說到斷言了。
因為我們這里使用的是Jasmine,因此就先放一下它的官網。

我們結合實例來說文檔
newBee.spec.js
describe("newBee單元測試", function() {it("減法函數測試", function() {var minus7 = minus(7);expect(minus7(6)).toBe(0);});
});
testKarma.spec.js
describe("testKarma單元測試", function() {it("如果函數測試", function() {expect(ifTest(true)).toBe(true);expect(ifTest(false)).toBe("涼涼");});it("回文函數測試", function() {expect(reverseStr('abc')).toEqual('cba');})});
基本的格式就是這樣的,下面來解釋一下
// 分組describe(), 這個是可以嵌套的,并且每個單獨的測試都有beforeAll, afterAll, beforeEach和afterEach
describe("這里寫測試群組的名稱", function(){// 具體的測試,it(), 當其中所有的斷言都為true時,則通過;否則失效。it('這里寫具體測試的名稱', function(){var a = true;// 期望, expect()。 匹配,to*()// 每個匹配方法在期望值和實際值之間執行邏輯比較// 它負責告訴jasmine斷言的真假,從而決定測試的成功或失敗// 木有錯,老子/老娘說啥就是啥expect(a).toBe(true); // 這是肯定斷言expect(!a).not.toBe(true); // 這是否定斷言// jasmine內置的匹配方法有很多,亦可自定義匹配方法// toBe()// toEqual()// toMatch()// toBeUndefined()// toBeNull()// toBeTruthy()// toContain()// toBeLessThan()// toBeCloseTo()// toThrowError()// 等等等等})
})
那么,測試方法寫完了,我們來實際運行一下測試吧。打開終端,輸入:
karma start
就會在終端看到

可以看到,我們的測試在Chrome和PhantomJS瀏覽器中分別測試了的5個方法,都有2個沒有通過測試,沒錯,我們當初在寫測試的時候故意寫錯了(這是真的)。
那么我們把測試修改成真值。
newBee.spec.js
describe("newBee單元測試", function() {it("減法函數測試", function() {var minus7 = minus(7);expect(minus7(6)).toBe(1);});
});
testKarma.spec.js
it("如果函數測試", function() {expect(ifTest(true)).toBe("熱熱");expect(ifTest(false)).toBe("涼涼");
});
結果是:

全部SUCCESS, 撒花。
到這里,一個基本的測試流程就走完了。然而,這并非終點。
其實,還能更進一步的。我們打開終端:
npm install karma-coverage --save-dev
然后打開karma.conf.js, 添加一些配置項
// 這里配置哪些文件需要統計測試覆蓋率,例如,如果你的所有代碼文件都在src文件夾中,你就需要如下配置
preprocessors: {"src/srcTest/*.js": "coverage"
},
// 新增coverageReporter選項
// 配置覆蓋率報告的查看方式,type查看類型,可以取值html、text等等,dir輸出目錄
coverageReporter: {dir: "docs/unit",reporters: [{type: "html",subdir: "report-html"}]
},
reporters: ['progress', "coverage"] // 沒錯,reporters里面新增了一個coverage
然后保存,再運行一次karma start
接著會發現你的項目里多了一個文件夾

用瀏覽器打開index.html。就會看到

這就是你所寫的js的測試覆蓋率。
這樣看起來是不是高大上了一些呢?
這里就有一個問題了。普通的js可以測試,可是我是寫Vue的啊,Vue組件怎么測試呢?很簡單,Vue官網有非常詳細的測試教程。甚至還有專用的測試工具和測試說明

彳亍口巴,你說的這些個單元測試看起來花里胡哨的,實際作用是什么呢?
單元測試的好處
- 單元測試不但會使你的工作完成得更輕松。而且會令你的設計會變得更好,甚至大大減少你花在調試上面的時間。
- 提高代碼質量
- 減少bug, 快速定位bug
- 使修改和重構可以更放心
- 顯得專業
單元測試的缺點
開發人員要花費時間在寫測試代碼上,然而又不會給你加工資...
小項目寫測試只能單純的增加開發時間和成本,然而又不會給你加工資...
我寫了測試除了懂測試的人能看懂,別人又不知道,然而還不會給你加工資...

別別別,別打我...你先聽我道(hu)理(jiao)講(man)完(chan)。
- 對于所編寫的代碼,你在調試上面畫了多少時間?
- 對于以前你自認為正確的代碼,而實際上這些代碼卻存在重大的bug,你花了多少時間在重新確認這些代碼上面?
- 對于一個別人報告的bug,你花了多少時間才找出導致這個bug的源碼位置?
對于那些沒有使用單元測試的程序員而言,上面這些問題所耗費的時間的是逐漸增加的,而且項目越深入,花費的時間越多;另一方面,適當的單元測試卻可以很大程度地減少這些時間,從而為你騰出足夠的時間來編寫所有的單元測試——甚至可能還有剩余的空閑時間。
更加真實的是,主流的框架必須要寫測試
不想當程序員的設計師不是好運維。 ----魯迅
作為一個程序員,如果你想要讓自己寫的框架放到github和npm上能夠為世界上的其他人所用。那么一個最基本的前提就是————代碼沒有BUG。可是,你的怎么向語言不通思維不同的人解釋你的JavaScript庫確實足夠健壯呢。這個時候就需要單元測試出場了。
主流前端框架雖然在所使用的測試庫(karma、jest、QUnit)和斷言庫(assert、jasmine、 chai)上略有差別,但Vue、React、Angular、Underscore甚至是jQuery都寫了單元測試。
來個石錘

下面我們看一看Vue的測試是怎么寫的:
git clone https://github.com/vuejs/vue.git
npm install
npm run test unit // 這里可以看到單元測試
npm run test // 這里就看全部的測試
Vue的測試覆蓋率為

舉例:v-show的測試
// import Vue from 'vue'describe('Directive v-show', () => {it('should check show value is truthy', () => {const vm = new Vue({template: '<div><span v-show="foo">hello</span></div>',data: { foo: true }}).$mount()expect(vm.$el.firstChild.style.display).toBe('')})it('should check show value is falsy', () => {const vm = new Vue({template: '<div><span v-show="foo">hello</span></div>',data: { foo: false }}).$mount()expect(vm.$el.firstChild.style.display).toBe('none')})it('should update show value changed', done => {const vm = new Vue({template: '<div><span v-show="foo">hello</span></div>',data: { foo: true }}).$mount()expect(vm.$el.firstChild.style.display).toBe('')vm.foo = falsewaitForUpdate(() => {expect(vm.$el.firstChild.style.display).toBe('none')vm.foo = {}}).then(() => {expect(vm.$el.firstChild.style.display).toBe('')vm.foo = 0}).then(() => {expect(vm.$el.firstChild.style.display).toBe('none')vm.foo = []}).then(() => {expect(vm.$el.firstChild.style.display).toBe('')vm.foo = null}).then(() => {expect(vm.$el.firstChild.style.display).toBe('none')vm.foo = '0'}).then(() => {expect(vm.$el.firstChild.style.display).toBe('')vm.foo = undefined}).then(() => {expect(vm.$el.firstChild.style.display).toBe('none')vm.foo = 1}).then(() => {expect(vm.$el.firstChild.style.display).toBe('')}).then(done)})it('should respect display value in style attribute', done => {const vm = new Vue({template: '<div><span v-show="foo" style="display:block">hello</span></div>',data: { foo: true }}).$mount()expect(vm.$el.firstChild.style.display).toBe('block')vm.foo = falsewaitForUpdate(() => {expect(vm.$el.firstChild.style.display).toBe('none')vm.foo = true}).then(() => {expect(vm.$el.firstChild.style.display).toBe('block')}).then(done)})it('should support unbind when reused', done => {const vm = new Vue({template:'<div v-if="tester"><span v-show="false"></span></div>' +'<div v-else><span @click="tester=!tester">show</span></div>',data: { tester: true }}).$mount()expect(vm.$el.firstChild.style.display).toBe('none')vm.tester = falsewaitForUpdate(() => {expect(vm.$el.firstChild.style.display).toBe('')vm.tester = true}).then(() => {expect(vm.$el.firstChild.style.display).toBe('none')}).then(done)})
})
只要你的測試覆蓋率足夠高,你就可以在著名的GitHub裝逼網站Codecov搞一個覆蓋率標簽了。就像下面這個:

怎么樣,這樣你所寫的框架,是不是就逼格滿滿?

所以你還在等什么,測不了吃虧,測不了上當,趕緊在自己的代碼中加入測試吧,~~只要998~~,代碼逼格帶回家!