我原本打算先一條一條列出測試給咱們的前端項目帶來的「先民血統」(保證項目的質量)和「勞動力解放」(自動化的力量)等諸多良好特性。但轉念一想,您既然來了確定是知道測試的種種好處,至少您也確定知道 100% 覆蓋率的測試真是亮瞎眼的裝逼利器。前端
你認爲測試難嗎?node
我以爲一點都不難,反而以爲處處在複製和粘貼測試代碼。這可不是代碼缺乏複用性,我只是懶和測試根本沒什麼套路可言。這句話,並非說寫測試像讀網文小說同樣沒內涵,而是力求簡潔直白。git
若是你聽過 TDD(測試驅動開發),對單元測試必定不會陌生,它是對一個模塊、類或函數進行斷言返回值的結果是否符合預期的結果。既然稱爲單元,也就是意味着咱們要進入每一個函數體,檢查每行代碼的執行狀況,是一種精細的測試。es6
看到本教程的題目,您可能已經據說過 mocha 和 jest 的大名,也許您已經使用 mocha 寫過一些測試。我之因此把長得跟一個媽生的兩兄弟放在一塊兒,就是由於他們實在太像了,既然要學,爲何咱們不能一次掌握兩個目前最火的單元測試框架呢。答案是,能夠的。github
那麼,咱們開始吧!正則表達式
特別聲明:如下全部例子都已經過測試,還能夠在 github/asVenus 查看本教程完整實例代碼幫助您更好學習。npm
首先,經過 npm 安裝單元測試框架 mocha 和 jest 與 chai 斷言庫。json
$ cnpm i mocha chai jest -D
複製代碼
咱們編寫一個待測試的 sum
函數,它很是簡單。api
// example/sum.js
module.exports = function sum (a, b) {
return a + b
}
複製代碼
而後,在 test
目錄下編寫咱們的第一個測試。數組
// test/sum.test.js
// 引入 chai 斷言庫
const expect = require('chai').expect
// 引入待測試的函數
const sum = require('../example/sum')
// describe 至關於一個測試的組,把一類相同的測試用例放在一塊兒
// 第一個參數是對測試組的說明
// 第二個參數僅是一個普通的回調,咱們在裏面放置一個或多個測試用例
describe('第一個測試', () => {
// it 就是一個測試用例
it('sum', () => expect(sum(1, 2)).to.be.equal(3))
// expect 接受一個 Actual,一個結果值
// to.be 是 chai 的提升可讀性的語言鏈(無關緊要)
// equal 是一個斷言函數,接受一個 Expected,一個期待值
// 當 Actual 經過 equal 斷言相等 Expected 時,測試經過
// 反之,失敗
})
複製代碼
如今,就是如今,打開您的命令行,輕輕地敲下 mocha
這個單詞,回車,mocha 會自動尋找到 test 文件並執行測試,你會看到。
是吧,咱們測試經過了,
如今咱們在 jest-test
目錄下編寫 jest 的測試。
// jest-test/sum.test.js
const sum = require('../example/sum')
describe('第一個測試', () => {
// jest 自帶斷言庫
it('sum', () => expect(sum(1, 2)).toBe(3))
})
複製代碼
而後咱們在命令行敲下 jest jest-test
,若是未發生意外你會看到。
若是你看到命令行拋出如下報錯:
Cannot find module 'source-map-support' from 'source-map-support.js'
你還須要執行npm i source-map-support -D
安裝 jest 的依賴。而後再次執行便可。
那麼,如今你應該能理解單元測試的本質了吧,就是斷言,就是輸入一個值而後根據一個斷言的規則最後看是否符合期待值。
前端業務中的異步場景多如牛毛,好比一個普通的回調、promise、監聽事件、執行動畫和接口調用等等。
mocha 和 jest 對於 promise
都有良好的支持,使測試更爲輕鬆。
// example/getUserData.js
// 模擬一個獲取用戶數據的接口調用
module.exports = function getUserData() {
return new Promise(resolve => {
// 一秒後異步成功,返回一個 'ok'
setTimeout(() => resolve('ok'), 1000)
})
}
複製代碼
mocha 和 jest 均可以直接將 promise
返回。
// mocha ☞ test/async.test.js
it('promise', () => {
return getUserData()
.then(data => {
expect(data).to.be.equal('ok')
})
})
複製代碼
// jest ☞ jest-test/async.test.js
it('promise', () => {
return getUserData()
.then(data => {
expect(data).toBe('ok')
})
})
複製代碼
另外,jest 還能夠經過 resolves/rejects
修飾符直接測試 promise
的成功或失敗狀態。
// jest ☞ jest-test/async.test.js
it('promise2', () => {
return expect(getUserData()).resolves.toBe('ok')
})
複製代碼
mocha 和 jest 的 async/await
用法。
// mocha ☞ test/async.test.js
it('sync', async () => {
expect(await getUserData()).to.be.equal('ok')
})
複製代碼
// jest ☞ jest-test/async.test.js
it('async', async () => {
expect(await getUserData()).toBe('ok')
})
複製代碼
對於普通的異步測試(好比一個定時器),咱們須要手動使用 done 函數通知測試結束。
// example/timer.js
module.exports = function timer(fn) {
setTimeout(fn, 1000)
}
複製代碼
// mocha ☞ test/async.test.js
it('done', done => {
timer(() => {
// 告訴 mocha 測試已經結束了!
// 注意,mocha 只會等待 2s
// 超時後,自動判斷爲測試失敗
done()
})
})
複製代碼
// jest ☞ jest-test/async.test.js
it('done', done => {
timer(() => {
// jest 等待爲 5s
done()
})
})
複製代碼
mocha 和 jest 擁有相同的鉤子機制,就連鉤子的名字也相同。
// 全部測試執行前觸發,只觸發一次
beforeAll()
// 全部測試執行結束後觸發,只觸發一次
afterAll()
// 在每一個測試執行前觸發
beforEach()
// 在每一個測試執行結束後觸發
afterEach()
複製代碼
要體現鉤子在測試中的重要地位,我簡單看一個測試的基本原則,就是每一個測試應當保持相互獨立。也就是說,當咱們測試一個複雜類的各類狀況時,類內部擁有本身的狀態。當一個測試結束時(改變了類的內部狀態),咱們忘記將其重置,在下一個測試中咱們很容易感到困惑(測試看起來應該是經過的可是卻失敗了)。由於,咱們產生了一個隱式的變化源,這將使得咱們須要額外花費精力記住每次測試後狀態的改變,這很容易讓測試變得困難並出錯。正確的作法應該是在 beforeEach
鉤子中從新 new
一個新的實例以重置狀態。
// example/car.js
module.exports = class Car {
constructor () {
this.oilMass = 10
}
start (mileage) {
this.oilMass = mileage * .1
}
addOil (rise) {
this.oilMass += rise
}
}
複製代碼
// jest ☞ jest-test/mock.js
const Car = require('../example/car')
describe('mock', () =>{
let car
beforeEach(() => car = new Car())
it('行駛', () => {
car.start(10)
expect(car.oilMass).toBe(1)
})
it('加油', () => {
car.addOil(1)
expect(car.oilMass).toBe(11)
})
})
// 另外,咱們還能夠單獨測試一個用例
// 而不用擔憂受到其餘測試的限制
複製代碼
鉤子和一個 it 測試單例沒什麼區別,你也能夠返回一個 promise 或使用 done 函數把同步的鉤子變爲異步的鉤子。另外還有一點,鉤子是具備做用域,當你放到 describe
(測試組)內,僅對組內的全部測試用例有效,當放到外面時將會當前文件中全部的測試有效。
// 對全部測試有效
beforeEach()
describe('測試組一', () => {
// 僅對測試一,測試二有效
afterEach()
it('測試一')
it('測試一')
})
describe('測試組二', () => {
it('測試三')
})
複製代碼
到此爲止,mocha 和 jest 的基本使用講完了,很輕鬆,對吧!我想您必定充滿信心。那麼,咱們再學一些具備挑戰的東西,它們各自的「高級」特性。
jest 的 mock,簡而言之,就是各類模擬,好比 Function(函數)、Timer(定時器) 等等。
咱們先看一下,mock function 的具體使用。
若是咱們的測試函數接受一個回調函數,這個回調函數在內部被調用或進一步傳遞,而這個過程,咱們根本無力進行測試。可是經過 mock function 模擬一個 fn 做爲測試的回調函數,咱們就有能力進行各類測試,好比測試 fn 的參數的個數、參數的值、是否被調用、調用次數以及調用的返回值等等。
// example/callback.js
module.exports = function callback (fn) {
return fn(1, 2)
}
複製代碼
// jest-test/mock.test.js
const callback = require('../example/callback')
describe('mock', () =>{
it('mock function', () => {
// 建立一個 mock function
const fn = jest.fn((a, b) => a + b)
// 傳入測試函數
callback(fn)
expect(fn).toHaveBeenCalled() // 是否被調用
expect(fn).toHaveBeenCalledTimes(1) // 是否只調用了一次
expect(fn).toHaveBeenCalledWith(1, 2) // 參數值
expect(fn).toHaveReturnedWith(3) // 返回值
})
})
複製代碼
原生的定時器函數(setTimeout, setInterval, clearTimeout, clearInterval)並非很方便測試,由於程序須要等待相應的延時。mock timer
經過覆蓋原生定時器函數,可讓您測試定時器是否被調用、傳入的參數是不是函數以及等待的時間、甚至還能夠控制時間流。
// example/timer.js
module.exports = function timer(fn) {
setTimeout(fn, 1000)
}
複製代碼
const timer = require('../example/timer')
// 讓 jest 覆蓋全局定時器並重置記錄狀態
beforeEach(() => jest.useFakeTimers())
it('mock timer', () => {
// 建立一個 mock function
const fn = jest.fn()
// 做爲 timer 的回調函數
timer(fn)
// 檢查 setTimeout 是否被調用了一次
expect(setTimeout).toHaveBeenCalledTimes(1)
// 檢查 setTimeout 傳入的兩個參數
// 是不是一個函數,是否要等待 1s
expect(setTimeout).toHaveBeenLastCalledWith(expect.any(Function), 1000)
// 目前,傳入到 timer 中的 fn 回調函數還沒被調用
expect(fn).not.toBeCalled()
// 那麼,咱們控制時間流
// 讓定時器立刻執行
jest.runAllTimers()
// 如今,fn 回調函數執行了!
expect(fn).toBeCalled();
expect(fn).toHaveBeenCalledTimes(1)
複製代碼
mocha 在測試報告的輸入格式下了大功夫,提供給您足夠多的選擇,這一節更像是對命令行界面設計的展覽。
在命令行執行如下命令,你將會看到不一樣輸入格式。
$ mocha --reporter 格式名
複製代碼
spec(默認)- 分層規格列表
dot - 點矩陣
json - json 對象
progress - 進度條
list - 規格式列表
tap - 測試任何協議
landing - unicode 的起落跑道
min - 最少信息輸入
nyan - 一隻 nyan 喵!
markdown - markdown 文檔 (github 口味)
你能夠重定向爲一個 md 文件
$ mocha --reporter markdown > test-reporter.md
複製代碼
jest 的斷言風格和 chai 的 expect 相同。
expect(actual).toBe(expected)
複製代碼
修飾符用來限定斷言的某種行爲,放在斷言函數或屬性的前面。(只列出部分經常使用的修飾符)
expect(true).not.toBe(false)
複製代碼
Jest 使用匹配器讓你能夠用各類方式測試你的代碼。這裏咱們介紹一些經常使用的匹配器快速開始您項目的測試。在 expect API 裏能夠查看到完整的列表。
toBeNull
只匹配 null。
expect(null).toBeNull()
複製代碼
toBeUndefined
只匹配 undefined。
expect(undefined).toBeUndefined()
複製代碼
toBeDefined
與 toBeUndefined 相反。
expect(1).toBeDefined()
複製代碼
toBeTruthy
匹配任何能夠類型轉換爲 true 的值。
expect(true).toBeTruthy()
expect('sunny').toBeTruthy()
expect(1).toBeTruthy()
expect([]).toBeTruthy()
複製代碼
toBeFalsy
匹配任何能夠類型轉換爲 false 的值。
expect(false).toBeFalsy()
expect('').toBeFalsy()
expect(0).toBeFalsy()
複製代碼
toBeNaN
只匹配 NaN。
expect(NaN).toBeNaN()
複製代碼
toHaveLength
檢查數組或字符串的 length
expect([1, 2, 3]).toHaveLength(3)
expect('abcd').toHaveLength(4)
複製代碼
toBe
使用 Object.js 方法進行相等比較。
expect(3).toBe(3)
expect(NaN).toBe(NaN) // 經過
複製代碼
toEqual
遞歸檢查對象或數組的每一個字段。
expect({name: 'sunny', age: 22}).toEqual({age: 22, name: 'sunny'})
expect(['sunny', 22]).toEqual(['sunny', 22])
複製代碼
toBeGreaterThan
檢查是否大於指定值。
expect(10).toBeGreaterThan(3)
複製代碼
toBeGreaterThanOrEqual
檢查是否大於等於指定值。
expect(10).toBeGreaterThanOrEqual(3)
expect(10).toBeGreaterThanOrEqual(10)
複製代碼
toBeLessThan
檢查是否小於指定值。
expect(10).toBeLessThan(20)
複製代碼
toBeLessThanOrEqual
檢查是否小於等於指定值。
expect(10).toBeLessThanOrEqual(20)
expect(10).toBeLessThanOrEqual(10)
複製代碼
toMatch
使用正則表達式匹配字符串。
expect('mocha and jest').toMatch(/jest/)
複製代碼
toContain
檢查一個數組或可迭代對象是否包含某項,還能夠檢查字符串是否包含每一個字符串。
expect([1, 2, 3]).toContain(3)
expect('mocha and jest').toContain('and')
複製代碼
chai 是一種支持多種風格(好比 expect 和 should)的斷言庫,咱們只介紹 expect。
const expect = require('chai').expect
expect(actual).to.be.equal(expected)
複製代碼
語言鏈是單純提供以提升斷言的可讀性,它們通常不提供測試功能(也就是無關緊要,寫不寫都行)
expect(true).not.be.to.equal(false)
expect('abc').length.be.to.equal(3)
複製代碼
chai 部分斷言和 jest 的匹配器使用上基本一致。這裏簡略地列出了一些 chai 經常使用的斷言,以供您使用和查閱。在 Assert - Chai 能夠查看到完整斷言的列表。
// 是否爲真值(轉換爲 true 的值)
ok
true
false
null
undefined
NaN
// 是否存在(即非 null 也非 undefined)
exist
// 是否爲空
// 對於數組和字符串,它檢查 length 屬性
// 對於對象,它檢查可枚舉屬性的數量
empty
// 斷言值的類型
a/an(type)
// 嚴格相等(===)
equal(value)
// 至關於 deep.equal
eql(value)
// 數值相關的斷言
above(value) // 大於
below(value) // 小於
most(value) // 不大於
least(value) // 不小於
within(start, finish) // 閉合區間
// 對象擁有某個爲名 name 的屬性
property(name, [value])
// 啓用 deep 修飾符後,還支持路徑查詢
deep.property('obj.a[1].c', 'sunny')
// 正則
match(regexp)
// 是否包含指定字符串
string(string)
複製代碼
jest 集成了覆蓋率測試,只須要在 babel.config.js
開啓 collectCoverage
字段便可。
// jest.config.js
module.exports = {
// 開啓覆蓋率測試
collectCoverage: true,
// 忽略的目錄
coveragePathIgnorePatterns: [
'node_modules'
]
}
複製代碼
而後,在命令行執行 jest jest-test
就會帶上覆蓋率測試的報告。因爲,本教程的實例代碼很簡單,咱們已經達成了 100% 的裝逼成就。覆蓋率報告會詳細的列出每次測試文件的
100% 覆蓋率的測試的確閃眼,但一味追求覆蓋率極可能會拔苗助長,極可能會改動代碼而自"覺得聰明地"繞過 Uncovered Line
經過覆蓋率測試,這種行爲很是危險,會讓測試質量變得不可靠,甚至使代碼爲了測試而編寫。因此,不管任何狀況下,覆蓋率測試只能做爲測試質量的一個參考標準,告訴咱們測試是否不夠精確、哪裏存在疏漏。這一點,咱們都應該銘記於心。
mocha 則須要第三方工具配合。
引入對 babel 的支持,可讓我使用 es6 moduel 的導入方式和一些提案語法而且能夠與基於 babel 作兼容的項目無縫銜接。
mocha:
$ npm i babel-core babel-preset-env babel-runtime -D
複製代碼
在項目根目錄下建立 babel 的配置文件 .babelrc
。
// .babelrc
{
"presets": [ "env" ],
"plugins": ["transform-runtime"]
}
複製代碼
在 test
測試目錄下建立 mocha 的配置文件 mocha.opts
。
# 測試報告輸出的格式
--reporter tap
# 遞歸測試全部的目錄和文件
--recursive
# 啓動觀察
# 只看文件發生改動自動從新啓動測試
--watch
# 開啓桌面通知
--growl
# 關鍵,讓 mocha 支持 babel
--require babel-core/register
複製代碼
注意,以上註釋只是爲了對每一個配置項進行說明。在您的配置文件中不能帶有任何註釋信息。
jest:
$ npm i babel-jest @babel/core @babel/preset-env -D
複製代碼
在項目根目錄下建立 babel 的配置文件 babel.config.js
// .babelrc
{
"presets": [
[
"@babel/preset-env",
{
"targets": {
"node": "current"
}
}
]
]
}
複製代碼
在根目錄下打開命令行,執行 jest --init
命令生成 jest 的配置文件 jest.config.js