一次學會使用 mocha & jest 編寫單元測試

單元測試是什麼?

我原本打算先一條一條列出測試給咱們的前端項目帶來的「先民血統」(保證項目的質量)和「勞動力解放」(自動化的力量)等諸多良好特性。但轉念一想,您既然來了確定是知道測試的種種好處,至少您也確定知道 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 文件並執行測試,你會看到。

image

是吧,咱們測試經過了,

如今咱們在 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,若是未發生意外你會看到。

image

若是你看到命令行拋出如下報錯:
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

jest 的 mock,簡而言之,就是各類模擬,好比 Function(函數)、Timer(定時器) 等等。

mock function

咱們先看一下,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)       // 返回值

  })
})
複製代碼

mock timer

原生的定時器函數(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 在測試報告的輸入格式下了大功夫,提供給您足夠多的選擇,這一節更像是對命令行界面設計的展覽。

在命令行執行如下命令,你將會看到不一樣輸入格式。

$ mocha --reporter 格式名
複製代碼

spec(默認)- 分層規格列表

image

dot - 點矩陣

image

json - json 對象

image

progress - 進度條

image

list - 規格式列表

image

tap - 測試任何協議

image

landing - unicode 的起落跑道

image

min - 最少信息輸入

image

nyan - 一隻 nyan 喵!

image

markdown - markdown 文檔 (github 口味)

你能夠重定向爲一個 md 文件

$ mocha --reporter markdown > test-reporter.md
複製代碼

查看本教程生成的 test-reporter

jest 斷言

jest 的斷言風格和 chai 的 expect 相同。

expect(actual).toBe(expected)
複製代碼

修飾符

修飾符用來限定斷言的某種行爲,放在斷言函數或屬性的前面。(只列出部分經常使用的修飾符)

  • not - 對斷言取反
  • resolves - promise 的成功狀態
  • rejects - promise 的失敗狀態
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

chai 是一種支持多種風格(好比 expect 和 should)的斷言庫,咱們只介紹 expect。

const expect = require('chai').expect

expect(actual).to.be.equal(expected)
複製代碼

語言鏈

語言鏈是單純提供以提升斷言的可讀性,它們通常不提供測試功能(也就是無關緊要,寫不寫都行)

  • to
  • be
  • been
  • is
  • that
  • which
  • and
  • has
  • have
  • with
  • at
  • of
  • same

修飾符

  • not - 對斷言取反
  • deep - 深度遞歸
  • length - 獲取長度
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% 的裝逼成就。覆蓋率報告會詳細的列出每次測試文件的

  • Stmts - 測試的有效代碼行數
  • Bracnh - 代碼分支
  • Funcs - 函數聲明以及調用等
  • Lines - 測試執行到行級的狀況
  • Uncovered Line #s - 當未到達 100% 時,會顯示具體哪一行沒測試到。

image

100% 覆蓋率的測試的確閃眼,但一味追求覆蓋率極可能會拔苗助長,極可能會改動代碼而自"覺得聰明地"繞過 Uncovered Line 經過覆蓋率測試,這種行爲很是危險,會讓測試質量變得不可靠,甚至使代碼爲了測試而編寫。因此,不管任何狀況下,覆蓋率測試只能做爲測試質量的一個參考標準,告訴咱們測試是否不夠精確、哪裏存在疏漏。這一點,咱們都應該銘記於心。

mocha 則須要第三方工具配合。

babel 支持

引入對 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

相關文章
相關標籤/搜索