單元測試整理
學習單元測試的時候接觸了很多概念karma、mocha、Jesmine、chai、expect、assert、should、sinon等,容易混亂,在此做個梳理。
1. 測試框架 Mocha、Jesmine
1.1 Mocha
Mocha是一個常用的JS測試框架,可以在瀏覽器和Nodejs環境使用。Mocha不帶斷言需要和斷言庫結合使用。專案中使用的也是Mocha+chai+sinon的結合。例如給ndfront元件寫的單元測試,詳情檢視github倉庫:github.com/baihexx/ndf…
特點:靈活,可擴充套件性好,可配合不同的斷言庫使用,但是自身整合度不高
1.2 Jesmine
Jesmine也是常用的測試框架,專案中沒有用這個。
特點:內建斷言庫,整合度高,方便支援非同步測試,但是靈活性差,斷言風格單一
2. 斷言庫
2.1 assert
assert模組是Node的內建模組,用於斷言。
官方API
常用API
eg:
var assert = requier('assert')
describe('desc1', function() {
it('desc2', function() {
assert(a === 1, '預期a的值是1')
})
})
複製程式碼
2.2 should.js
should.js是個第三方斷言庫,常和Mocha聯合使用。
- 使用方法1:
requier('should'): 擴充套件Object.prototype,增加should屬性,所有Object可以直接獲取should使用,eg:
var should = require('should');
(5).should.be.exactly(5).and.be.a.Number();
var a = null
a.should.not.be.ok() // 報錯
複製程式碼
- 使用方法2:
若是undefined或者null,並沒有繼承Object的原型鏈,沒有should屬性可用,可採用如下方法:
var should = require('should/as-function');
var a = null
should(a).not.be.ok() // pass
should(10).be.exactly(5).and.be.a.Number();
複製程式碼
2.3 Chai
官網API: 安裝方法等檢視官網
Chai是個斷言庫,常和Mocha結合使用。他有多種斷言風格(assertion style):assert, expect, should
- assert斷言風格:和nodejs的assert模組類似(多了寫語法糖),是一種非鏈式語言風格 eg:
var assert = requier('chai').assert
assert.notEqual(3, 4, 'these numbers are not equal')
複製程式碼
- expect斷言風格:expect和should都是BDD風格,是一種鏈式語言風格, 連線詞有 to,be,been,is等自然語言, eg:
var expect = requier('Chai').expect
expect([1, 2, 3]).to.be.an('array').that.includes(2)
複製程式碼
- should斷言風格:should()擴充套件了Object.prototype,增加了should屬性,使用方法如下。eg:
var should = require('chai').should() //actually call the function
var foo = 'bar'
foo.should.be.a('string')
foo.should.equal('bar')
foo.should.have.lengthOf(3)
複製程式碼
(注意should對IE相容性不好)
3. 測試執行工具:karma
定義:A simple tool that allows you to execute JavaScript code in multiple real browsers. The main purpose of Karma is to make your test-driven development easy, fast, and fun.
Karma不是測試框架,也不是斷言庫,他會開啟一個HTTP服務,將測試檔案生成一個Html檔案,在瀏覽器內執行、除錯。Karma不指定測試框架,通過外掛和Mocha、Jesmine、QUnit都可以結合使用。
配置項較多,根據官網說明配置,並不難。
測試覆蓋率:根據提示安裝、配置即可生成覆蓋率報告
4. 測試輔助工具:Sinon
為什麼需要Sinon?在做單元測試的時候,我們會發現我們要測試的方法會引用很多外部依賴的物件,比如:(傳送郵件,網路通訊,記錄Log, 檔案系統之類的),而我們沒法控制這些外部依賴的物件。例如:前端專案通常是用Ajax去服務端請求資料,得到資料之後做進一步的處理。但是做單元測試的時候通常不真的去服務端請求資料,不僅麻煩,可能服務端介面還沒做好,這種不確定的依賴使得測試變得複雜。所以我們需要模擬這個請求資料的過程,Sinon用來解決這個問題。
Sinon的工作本質是“測試替身”,測試替身用來替換測試中的部分程式碼,使得測試複雜程式碼變得簡單。
Sinon提供了三個功能:示例講解看入門文章,不再贅述
- spy(間諜):提供函式呼叫的資訊,但不會改變函式的行為
- stub:與spies類似,但是會完全替換目標函式。這使得一個被stubbed的函式可以做任何你想要的 —— 例如丟擲一個異常,返回某個特定值等等。
- mock:通過組合spies和stubs,使替換一個完整物件更容易
eg:admin/misc/user.js: user.getUser() (看不懂的隨便看看,這是實際的專案程式碼單元測試)
user.getUser函式用來獲取使用者資訊,使用者輸入工號後向服務端請求資料,我們的測試用例重點在前端程式碼,不應依賴服務端才可測試,so 應該模擬ajax請求。
(1)nd-spa中ajax.js請求程式碼如下:實際的請求函式為:request.get, so應該mock request的get函式
(2)測試用例程式碼如下:
- sinon.stub(obj, functionname, mockFun)
- ajax.js中的請求程式碼呼叫的set(),send(),end()函式實際是mock中定義的函式,並沒有做實際的後端請求,end的callback直接返回資料
- 注意L11:設定函式最大時長,寫成function(done)形式才可用this.timeout,否則es6的箭頭函式中this是window
5. 多步驟測試用例(實際專案記錄,可不看,估計看不明白)
使用者在前端頁面中通常是通過點選滑鼠期待某種效果,這個過程通常不做單元測試,因為複雜度較高,且頁面變動較快,價效比很低。但是專案中某些公共的業務元件,需求變動小,步驟相對簡單,但使用又非常多,例如social管理後臺中的搜尋使用者admin/misc/user.js:user.autoComplete(),完整的過程是:使用者輸入使用者資訊,然後選擇搜尋到的匹配使用者,再點選搜尋到的使用者,該輸入框的值變為選擇的使用者。這個過程如何編寫單元測試。
這裡涉及到多步驟的模擬。
(1)util.js 封裝多步執行函式
// utils.js
export function triggerHTMLEvents (target, event, process) {
const e = document.createEvent('HTMLEvents')
e.initEvent(event, true, true)
if (process) process(e)
target.dispatchEvent(e)
return e
}
export function triggerMouseEvents (target, event, process) {
const e = document.createEvent('MouseEvents')
e.initEvent(event, true, true)
if (process) process(e)
target.dispatchEvent(e)
return e
}
export function triggerUIEvents (target, event, process) {
const e = document.createEvent('UIEvents')
e.initEvent(event, true, true)
if (process) process(e)
target.dispatchEvent(e)
return e
}
// timeout: 執行間隔可設定
function createStep ({ step, timeout }) {
return new Promise((resolve, reject) => {
setTimeout(() => {
try {
resolve(step())
} catch (err) {
reject(err)
}
}, timeout || 0)
})
}
// 多步驟執行函式, 執行步驟封裝在arr陣列中
export function runSteps (arr) {
if (arr.length === 0) {
return
}
let firstStep = createStep(arr[0])
const others = arr.splice(1)
others.forEach(item => {
firstStep = firstStep.then(() => {
return createStep(item)
})
})
}
複製程式碼
(2)使用
L116:給input設定值L117:觸發input的change事件,執行nd-autocomplete/src/input.js中change事件,如何呼叫的看autocomplete元件 L122:點選搜尋列表的item,將值設定到input,然後驗證input的值
過程中遇到一個額外的問題,在此記錄下,備忘
(1)user中getUsers函式用到ucOrgId,檢視程式碼(var ucOrgId = auth.getAuth('uc_org_id') 且僅僅在登入程式碼中有auth.setAuth())可知該組織id資訊必須有,但是測試頁面中沒有登入,獲取不到該資訊,so,造登入資料,並設定到auth中。
(2)造登入資料並設定到auth中時遇到一個問題:若在user.spec.js中 引入auth,then auth.setAuth(...), 結果不對
原因:user.js中在開頭就執行了獲取ucOrgId的函式,我們在測試程式碼中先引入auth和user,這時ucOrgId已經獲取了,且是空值,即使假造登入資料的函式寫在import user之前也是沒用,因為es6有提升的功能,總是先執行import,導致函式執行在後面,解決方法是:將設定登入資料的函式寫在單獨的檔案中,使用import的方法,且在user之前inport,就會達到先執行設定登入資料的效果。(具體看程式碼更清晰)
(import的提升再複習下)