Jest 是由 Facebook 開源出來的一個測試框架,它集成了斷言庫、mock巍耗、快照測試秋麸、覆蓋率報告等功能。它非常適合用來測試 React 代碼炬太,但不僅僅如此灸蟆,所有的 js 代碼都可以使用 Jest 進行測試。
本文全面的介紹如何使用 Jest亲族,讓后來者輕松上手炒考。文中會選取重點部分直接貼出代碼,比較簡單的部分則不會孽水,主要是寫到后面的時候發(fā)現(xiàn)貼的代碼有點多票腰,沒什么意思,所有的代碼已上傳到 Github女气,可以自行查閱杏慰。
安裝
使用 yarn
安裝 Jest:
$ yarn add --dev jest
或使用 npm
:
$ npm i -D jest
其中 --dev
和 -D
參數(shù)指明作為 devDependencies,這樣該依賴只會在開發(fā)環(huán)境下安裝炼鞠,在生成環(huán)境下則不會缘滥。
在 package.json
文件中添加下面的內容:
"scripts": {
"test": "jest"
}
這樣我們就可以通過 yarn test
或 npm test
執(zhí)行測試代碼。
同樣地谒主,你也可以選擇全局安裝 Jest:
$ yarn global add jest
$ # or npm i -g jest
這樣你就可以直接在命令行使用 jest
命令朝扼。如果你是本地安裝,但是也想在命令行使用 jest
霎肯,可以通過 node_modules/.bin/webpack
訪問它的 bin 版本擎颖,如果你的 npm 版本在 5.2.0 以上,你也可以通過 npx jest
訪問观游。
使用 Babel
如果你在代碼中使用了新的語法特性搂捧,而當前 Node 版本不支持,則需要使用 Babel 進行轉義懂缕。
$ npm i -D babel-jest babel-core babel-preset-env
注意:如果你使用 babel 7允跑,安裝 babel-jest 的同時還需要安裝其他依賴:
npm i -D babel-jest 'babel-core@^7.0.0-0' @babel/core
Jest 默認使用 babel-jest(需要安裝) 進行代碼轉義,如果你需要添加額外的預處理器搪柑,則需要在 Jest 配置文件中顯示的定義 babel-jest 作為 JavaScript 處理器(因為一旦添加了 transform 配置聋丝,babel-jest 就不會自動載入了):
"transform": {
"^.+\\.jsx?$": "babel-jest"
},
我們還需在根目錄下創(chuàng)建 .babelrc
文件:
{
"presets": [
"env"
]
}
我這里只使用了 babel-preset-env 預設,如果需要其他的轉換工碾,見 babel弱睦。
基本用法
我們從一個基本的 Math 模塊開始。首先創(chuàng)建一個 math.js
文件:
// basic/math.js
const sum = (a, b) => a + b
const mul = (a, b) => a * b
const sub = (a, b) => a - b
const div = (a, b) => a / b
export { sum, mul, sub, div }
要測試這個 Math 模塊是否正確倚喂,我們需要編寫測試代碼每篷。通常瓣戚,測試文件與所要測試的源碼文件同名,但是后綴名為 .test.js
或者 .spec.js
焦读。我們這里則創(chuàng)建一個 math.test.js
文件:
// basic/math.test.js
import { sum, mul, sub, div } from './math'
test('Adding 1 + 1 equals 2', () => {
expect(sum(1, 1)).toBe(2)
})
test('Multiplying 1 * 1 equals 1', () => {
expect(mul(1, 1)).toBe(1)
})
test('Subtracting 1 - 1 equals 0', () => {
expect(sub(1, 1)).toBe(0)
})
test('Dividing 1 / 1 equals 1', () => {
expect(div(1, 1)).toBe(1)
})
執(zhí)行 npm test
Jest 將會執(zhí)行所有匹配的測試文件子库,并最終返回測試結果:
在編輯器中運行
很多編輯器都能支持 Jest,如:Webstorm矗晃、VS Code仑嗅、Atom 等。這里簡單地介紹下如何在 Webstorm 和 VS Code 中運行张症。
Webstorm
Webstorm 可能出現(xiàn)找不到變量等問題仓技,在 Preferences | Languages & Frameworks | JavaScript | Libraries
中點擊 Download
, 然后選擇 Jest 并下載即可。
Webstorm 可以識別測試代碼俗他,在編輯器中點擊“相應的運行按鈕”即可運行脖捻,或使用快捷鍵 ctrl+shift+R
(mac 中)。具體的操作可以參考我之前寫的 Node.js 中 使用 Mocha 進行單元測試的博客兆衅。
VS Code
要想在 VS Code 中運行地沮,我們需要安裝 Jest 插件。
插件安裝完成后羡亩,如果你安裝了 Jest摩疑,它會自動的運行測試代碼。你可以可以手動的運行通過 Jest: Start Runner 命令畏铆,它會執(zhí)行測試代碼并在文件發(fā)生修改后重新運行雷袋。
匹配器
匹配器用來實現(xiàn)斷言功能。在前面的例子中辞居,我們只使用了 toBe()
匹配器:
test('Adding 1 + 1 equals 2', () => {
expect(sum(1, 1)).toBe(2)
})
在此代碼中昆稿,expect(sum(1, 1))
返回一個“期望”對象廉沮,.toBe(2)
是匹配器毅哗。匹配器將 expect()
的結果(實際值)與自己的參數(shù)(期望值)進行比較剪返。當 Jest 運行時,它會跟蹤所有失敗的匹配器倚搬,并打印出錯誤信息。
常用的匹配器如下:
-
toBe
使用 Object.is 判斷是否嚴格相等乾蛤。 -
toEqual
遞歸檢查對象或數(shù)組的每個字段每界。 -
toBeNull
只匹配null
。 -
toBeUndefined
只匹配undefined
家卖。 -
toBeDefined
只匹配非undefined
眨层。 -
toBeTruthy
只匹配真。 -
toBeFalsy
只匹配假上荡。 -
toBeGreaterThan
實際值大于期望趴樱。 -
toBeGreaterThanOrEqual
實際值大于或等于期望值 -
toBeLessThan
實際值小于期望值馒闷。 -
toBeLessThanOrEqual
實際值小于或等于期望值。 -
toBeCloseTo
比較浮點數(shù)的值叁征,避免誤差纳账。 -
toMatch
正則匹配。 -
toContain
判斷數(shù)組中是否包含指定項捺疼。 -
.toHaveProperty(keyPath, value)
判斷對象中是否包含指定屬性疏虫。 -
toThrow
判斷是否拋出指定的異常。 -
toBeInstanceOf
判斷對象是否是某個類的實例啤呼,底層使用instanceof
卧秘。
所有的匹配器都可以使用 .not
取反:
test('Adding 1 + 1 does not equal 3', () => {
expect(1 + 1).not.toBe(3)
})
對于 Promise 對象,我們可以使用 .resolves
和 .rejects
:
// .resolves
test('resolves to lemon', () => {
// make sure to add a return statement
return expect(Promise.resolve('lemon')).resolves.toBe('lemon')
})
// .rejects
test('rejects to octopus', () => {
// make sure to add a return statement
return expect(Promise.reject(new Error('octopus'))).rejects.toThrow(
'octopus',
)
})
異步測試
JavaScript 代碼中常常會包含異步代碼官扣,當測試異步代碼時翅敌,Jest 需要知道什么時候異步代碼執(zhí)行完成,在異步代碼執(zhí)行完之前惕蹄,它會去執(zhí)行其他的測試代碼蚯涮。Jest 提供了多種方式測試異步代碼。
回調函數(shù)
當執(zhí)行到測試代碼的尾部時焊唬,Jest 即認為測試完成恋昼。因此,如果存在異步代碼赶促,Jest 不會等待回調函數(shù)執(zhí)行液肌。要解決這個問題,在測試函數(shù)中我們接受一個參數(shù)叫做 done
鸥滨,Jest 將會一直等待嗦哆,直到我們調用 done()
。如果一直不調用 done()
婿滓,則此測試不通過老速。
// async/fetch.js
export const fetchApple = (callback) => {
setTimeout(() => callback('apple'), 300)
}
// async/fetch.test.js
import { fetchApple } from './fetch'
test('the data is apple', (done) => {
expect.assertions(1)
const callback = data => {
expect(data).toBe('apple')
done()
}
fetchApple(callback)
})
expect.assertions(1)
驗證當前測試中有 1 處斷言會被執(zhí)行,在測試異步代碼時凸主,能確遍偃回調中的斷言被執(zhí)行。
Promise
如果異步代碼返回 Promise 對象卿吐,那我們在測試代碼直接返回該 Promise 即可旁舰,Jest 會等待其 resolved,如果 rejected 則測試不通過嗡官。
test('the data is banana', () => {
expect.assertions(1)
return fetchBanana().then(data => expect(data).toBe('banana'))
})
如果期望 promise 是 rejected 狀態(tài)箭窜,可以使用 .catch()
:
test('the fetch fails with an error', () => {
expect.assertions(1)
return fetchError().catch(e => expect(e).toMatch('error'))
})
除此之外,還可以使用上文中提到的 .resolves
和 .rejects
衍腥。
Async/Await
如果異步代碼返回 promise磺樱,我們還可以使用 async/await:
test('async: the data is banana', async () => {
expect.assertions(1)
const data = await fetchBanana()
expect(data).toBe('banana')
})
test('async: the fetch fails with an error', async () => {
expect.assertions(1)
try {
await fetchError()
} catch (e) {
expect(e).toMatch('error')
}
})
也可以將 aysnc/awiat 與 .resolves
或 .rejects
結合:
test('combine async with `.resolves`', async () => {
expect.assertions(1)
await expect(fetchBanana()).resolves.toBe('banana')
})
鉤子函數(shù)
Jest 為我們提供了四個測試用例的鉤子:beforeAll()
纳猫、afterAll()
、beforeEach()
竹捉、afterEach()
芜辕。
beforeAll()
和 afterAll()
會在所有測試用例之前和所有測試用例之后執(zhí)行一次。
beforeEach()
和 afterEach()
會在每個測試用例之前和之后執(zhí)行活孩。
分組
我們可以使用 describe
將測試用例分組物遇,在 describe
塊中的鉤子函數(shù)只作用于塊內的測試用例:
beforeAll(() => console.log('1 - beforeAll')) // 1
afterAll(() => console.log('1 - afterAll')) // 12
beforeEach(() => console.log('1 - beforeEach')) // 2,6
afterEach(() => console.log('1 - afterEach')) // 4,10
test('', () => console.log('1 - test')) // 3
describe('Scoped / Nested block', () => {
beforeAll(() => console.log('2 - beforeAll')) // 5
afterAll(() => console.log('2 - afterAll')) // 11
beforeEach(() => console.log('2 - beforeEach')) // 7
afterEach(() => console.log('2 - afterEach')) // 9
test('', () => console.log('2 - test')) // 8
})
需要注意的是,頂級的 beforeEach
會在 describe
塊內的 beforeEach
之前執(zhí)行憾儒。
Jest 會先執(zhí)行 describe
塊內的操作询兴,等 describe
塊內的操作執(zhí)行完畢后,按照出現(xiàn)在 describe
中的先后順序執(zhí)行測試用例起趾,因此初始化和銷毀操作應該放在鉤子函數(shù)中運行诗舰,而不是 describe
塊內:
describe('outer', () => {
console.log('describe outer-a') // 1
describe('describe inner 1', () => {
console.log('describe inner 1') // 2
test('test 1', () => {
console.log('test for describe inner 1') // 6
expect(true).toEqual(true)
})
})
console.log('describe outer-b') // 3
test('test 1', () => {
console.log('test for describe outer') // 7
expect(true).toEqual(true)
})
describe('describe inner 2', () => {
console.log('describe inner 2') // 4
test('test for describe inner 2', () => {
console.log('test for describe inner 2') // 8
expect(false).toEqual(false)
})
})
console.log('describe outer-c') // 5
})
Mocks
在測試中,mock 可以讓你更方便的去測試依賴于數(shù)據(jù)庫训裆、網(wǎng)絡請求眶根、文件等外部系統(tǒng)的函數(shù)。
Jest 內置了 mock 機制边琉,提供了多種 mock 方式已應對各種需求属百。
Mock 函數(shù)
函數(shù)的 mock 非常簡單,調用 jest.fn()
即可獲得一個 mock 函數(shù)变姨。
Mock 函數(shù)有一個特殊的 .mock
屬性族扰,保存著函數(shù)的調用信息。.mock
屬性還會追蹤每次調用時的 this
定欧。
// mocks/forEach.js
export default (items, callback) => {
for (let index = 0; index < items.length; index++) {
callback(items[index])
}
}
import forEach from './forEach'
it('test forEach function', () => {
const mockCallback = jest.fn(x => 42 + x)
forEach([0, 1], mockCallback)
// The mock function is called twice
expect(mockCallback.mock.calls.length).toBe(2)
// The first argument of the first call to the function was 0
expect(mockCallback.mock.calls[0][0]).toBe(0)
// The first argument of the second call to the function was 1
expect(mockCallback.mock.calls[1][0]).toBe(1)
// The return value of the first call to the function was 42
expect(mockCallback.mock.results[0].value).toBe(42)
})
除了 .mock
之外渔呵,Jest 還未我們提供了一些匹配器用來斷言函數(shù)的執(zhí)行,它們本身只是檢查 .mock
屬性的語法糖:
// The mock function was called at least once
expect(mockFunc).toBeCalled();
使用 mockReturnValue
和 mockReturnValueOnce
可以 mock 函數(shù)的返回值砍鸠。
當我們需要為 mock 函數(shù)增加一些邏輯時扩氢,可以使用 jest.fn()
、mockImplementation
或者 mockImplementationOnce
mock 函數(shù)的實現(xiàn)爷辱。
還可以使用 mockName
還給 mock 函數(shù)命名录豺,如果沒有命名,輸出的日志默認就會打印 jest.fn()
饭弓。
Mock 定時器
Jest 可以 Mock 定時器以使我們在測試代碼中控制“時間”巩检。調用 jest.useFakeTimers()
函數(shù)可以偽造定時器函數(shù),定時器中的回調函數(shù)不會被執(zhí)行示启,使用 setTimeout.mock
等可以斷言定時器執(zhí)行情況。當在測試中有多個定時器時领舰,執(zhí)行 jest.useFakeTimers()
可以重置內部的計數(shù)器夫嗓。
執(zhí)行 jest.runAllTimers();
可以“快進”直到所有的定時器被執(zhí)行迟螺;執(zhí)行 jest.runOnlyPendingTimers()
可以使當前正在等待的定時器被執(zhí)行,用來處理定時器中設置定時器的場景舍咖,如果使用 runAllTimers
會導致死循環(huán)矩父;執(zhí)行 jest.advanceTimersByTime(msToRun:number)
,可以“快進”執(zhí)行的毫秒數(shù)排霉。
Mock 模塊
模塊的 mock 主要有兩種方式:
- 使用
jest.mock(moduleName, factory, options)
自動 mock 模塊窍株,jest 會自動幫我們 mock 指定模塊中的函數(shù)。其中攻柠,factory
和options
參數(shù)是可選的球订。factory
是一個模塊工廠函數(shù),可以代替 Jest 的自動 mock 功能瑰钮;options
用來創(chuàng)建一個不存在的需要模塊冒滩。 - 如果希望自己 mock 模塊內部函數(shù),可以在模塊平級的目錄下創(chuàng)建
__mocks__
目錄浪谴,然后創(chuàng)建相應模塊的 mock 文件开睡。對于用戶模塊和 Node 核心模塊(如:fs、path)苟耻,我們仍需要在測試文件中顯示的調用jest.mock()
篇恒,而其他的 Node 模塊則不需要。
此外凶杖,在 mock 模塊時胁艰,jest.mock()
會被自動提升到模塊導入前調用。
對于類的 mock 基本和模塊 mock 相同官卡,支持自動 mock蝗茁、手動 mock 以及調用帶模塊工廠參數(shù)的 jest.mock()
,還可以調用 jest.mockImplementation()
mock 構造函數(shù)寻咒。
快照測試
快照測試是 Jest 提供的一個相當棒的 UI 測試功能哮翘,它會記錄 React 結構樹快照或其他可序列化的值,并與當前測試的值進行比較毛秘,如果不匹配則給出錯誤提示饭寺。快照應該被當做代碼來對待叫挟,它需要被提交到版本庫并進行 Review艰匙。
如果組件渲染結果發(fā)生變化,測試將會失敗抹恳。當組件正常調整時员凝,我們可以調用 jest -u
更新快照。在監(jiān)控模式下奋献,我們可以通過交互式的命令更新快照健霹。
下面通過一個簡單的 text 組件來測試一下:
// Text.js
import React from 'react'
export default ({className, children}) => {
return (
<span className={className}>{children}</span>
)
}
除了 react 我們還需要安裝依賴:npm i -D babel-preset-react react-test-renderer
旺上,其中 babel-preset-react
預設用來解析 jsx 語法,需要添加到 babel 配置中糖埋。
測試代碼如下:
// Text.test.js
import React from 'react'
import renderer from 'react-test-renderer'
import Text from './Text'
it('render correctly', () => {
const tree = renderer
.create(<Text className="success">Snapshot testing</Text>)
.toJSON()
expect(tree).toMatchSnapshot()
})
執(zhí)行測試代碼后宣吱,會生成如下快照:
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`render correctly 1`] = `
<span
className="success"
>
Snapshot testing
</span>
`;
如果后續(xù)修改導致組件渲染結果發(fā)生變化,快照將會不匹配瞳别,測試則不通過征候。
Jest 命令行
jest 命令行工具有有用的選項。運行 jest -h
可以查看所有可用的選項祟敛。所有的 Jest 的 配置項都可以通過命令行來指定疤坝。
基本用法:jest [--config=<pathToConfigFile>] [TestPathPattern]
生成配置信息:jest --init
運行符合指定用模板或文件名的測試︰jest path/to/my-test.js
啟動監(jiān)視模式︰jest --watch
生成覆蓋率報告:jest --coverage
Jest 配置
Jest 的一個理念是提供一套完整集成的“零配置”測試體驗,開發(fā)人員可以直接上手編寫測試用例垒棋。它為我們集成了測試常用的工具卒煞,多數(shù)情況下使用默認配置或少量的調整即可。
Jest 的配置可以定義在 package.json
或 jest.config.js
文件中或通過命令行參數(shù) --config <path/to/js|json>
叼架。配置并不是必須的畔裕,具體內容見文檔,按需取用即可乖订。
PS:Jest 中 testURL
的默認值是 about:blank
扮饶,在 jsdom 環(huán)境下運行會報錯,設置了 testURL
為一個有效的 URL 后能夠避免這個問題乍构,如:http://localhost
甜无。