测试框架 Jest 实例教程
Jest 是由 Facebook 開源出來的一個測試框架,它集成了斷言庫、mock、快照測試、覆蓋率報告等功能。它非常適合用來測試 React 代碼,但不僅僅如此,所有的 js 代碼都可以使用 Jest 進行測試。
本文全面的介紹如何使用 Jest,讓后來者輕松上手。文中會選取重點部分直接貼出代碼,比較簡單的部分則不會,主要是寫到后面的時候發現貼的代碼有點多,沒什么意思,所有的代碼已上傳到 Github,可以自行查閱。
安裝
使用 yarn 安裝 Jest:
$ yarn add --dev jest 復制代碼或使用 npm:
$ npm i -D jest 復制代碼其中 --dev 和 -D 參數指明作為 devDependencies,這樣該依賴只會在開發環境下安裝,在生成環境下則不會。
在 package.json 文件中添加下面的內容:
"scripts": {"test": "jest" } 復制代碼這樣我們就可以通過 yarn test 或 npm test 執行測試代碼。
同樣地,你也可以選擇全局安裝 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" }, 復制代碼我們還需在根目錄下創建 .babelrc 文件:
{"presets": ["env"] } 復制代碼我這里只使用了 babel-preset-env 預設,如果需要其他的轉換,見 babel。
基本用法
我們從一個基本的 Math 模塊開始。首先創建一個 math.js 文件:
// basic/math.jsconst sum = (a, b) => a + b const mul = (a, b) => a * b const sub = (a, b) => a - b const div = (a, b) => a / bexport { sum, mul, sub, div } 復制代碼要測試這個 Math 模塊是否正確,我們需要編寫測試代碼。通常,測試文件與所要測試的源碼文件同名,但是后綴名為 .test.js 或者 .spec.js。我們這里則創建一個 math.test.js 文件:
// basic/math.test.jsimport { 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) }) 復制代碼執行 npm test Jest 將會執行所有匹配的測試文件,并最終返回測試結果:
在編輯器中運行
很多編輯器都能支持 Jest,如:Webstorm、VS Code、Atom 等。這里簡單地介紹下如何在 Webstorm 和 VS Code 中運行。
Webstorm
Webstorm 可能出現找不到變量等問題,在 Preferences | Languages & Frameworks | JavaScript | Libraries 中點擊 Download, 然后選擇 Jest 并下載即可。
Webstorm 可以識別測試代碼,在編輯器中點擊“相應的運行按鈕”即可運行,或使用快捷鍵 ctrl+shift+R(mac 中)。具體的操作可以參考我之前寫的 Node.js 中 使用 Mocha 進行單元測試的博客。
VS Code
要想在 VS Code 中運行,我們需要安裝 Jest 插件。
插件安裝完成后,如果你安裝了 Jest,它會自動的運行測試代碼。你可以可以手動的運行通過 Jest: Start Runner 命令,它會執行測試代碼并在文件發生修改后重新運行。
匹配器
匹配器用來實現斷言功能。在前面的例子中,我們只使用了 toBe() 匹配器:
test('Adding 1 + 1 equals 2', () => {expect(sum(1, 1)).toBe(2) }) 復制代碼在此代碼中,expect(sum(1, 1)) 返回一個“期望”對象,.toBe(2) 是匹配器。匹配器將 expect() 的結果(實際值)與自己的參數(期望值)進行比較。當 Jest 運行時,它會跟蹤所有失敗的匹配器,并打印出錯誤信息。
常用的匹配器如下:
- toBe 使用 Object.is 判斷是否嚴格相等。
- toEqual 遞歸檢查對象或數組的每個字段。
- toBeNull 只匹配 null。
- toBeUndefined 只匹配 undefined。
- toBeDefined 只匹配非 undefined。
- toBeTruthy 只匹配真。
- toBeFalsy 只匹配假。
- toBeGreaterThan 實際值大于期望。
- toBeGreaterThanOrEqual 實際值大于或等于期望值
- toBeLessThan 實際值小于期望值。
- toBeLessThanOrEqual 實際值小于或等于期望值。
- toBeCloseTo 比較浮點數的值,避免誤差。
- toMatch 正則匹配。
- toContain 判斷數組中是否包含指定項。
- .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 statementreturn expect(Promise.resolve('lemon')).resolves.toBe('lemon') })// .rejects test('rejects to octopus', () => {// make sure to add a return statementreturn expect(Promise.reject(new Error('octopus'))).rejects.toThrow('octopus',) }) 復制代碼異步測試
JavaScript 代碼中常常會包含異步代碼,當測試異步代碼時,Jest 需要知道什么時候異步代碼執行完成,在異步代碼執行完之前,它會去執行其他的測試代碼。Jest 提供了多種方式測試異步代碼。
回調函數
當執行到測試代碼的尾部時,Jest 即認為測試完成。因此,如果存在異步代碼,Jest 不會等待回調函數執行。要解決這個問題,在測試函數中我們接受一個參數叫做 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 處斷言會被執行,在測試異步代碼時,能確保回調中的斷言被執行。
Promise
如果異步代碼返回 Promise 對象,那我們在測試代碼直接返回該 Promise 即可,Jest 會等待其 resolved,如果 rejected 則測試不通過。
test('the data is banana', () => {expect.assertions(1)return fetchBanana().then(data => expect(data).toBe('banana')) }) 復制代碼如果期望 promise 是 rejected 狀態,可以使用 .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') }) 復制代碼鉤子函數
Jest 為我們提供了四個測試用例的鉤子:beforeAll()、afterAll()、beforeEach()、afterEach()。
beforeAll() 和 afterAll() 會在所有測試用例之前和所有測試用例之后執行一次。 beforeEach() 和 afterEach() 會在每個測試用例之前和之后執行。
分組
我們可以使用 describe 將測試用例分組,在 describe 塊中的鉤子函數只作用于塊內的測試用例:
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')) // 5afterAll(() => console.log('2 - afterAll')) // 11beforeEach(() => console.log('2 - beforeEach')) // 7afterEach(() => console.log('2 - afterEach')) // 9test('', () => console.log('2 - test')) // 8 }) 復制代碼需要注意的是,頂級的 beforeEach 會在 describe 塊內的 beforeEach 之前執行。
Jest 會先執行 describe 塊內的操作,等 describe 塊內的操作執行完畢后,按照出現在 describe 中的先后順序執行測試用例,因此初始化和銷毀操作應該放在鉤子函數中運行,而不是 describe 塊內:
describe('outer', () => {console.log('describe outer-a') // 1describe('describe inner 1', () => {console.log('describe inner 1') // 2test('test 1', () => {console.log('test for describe inner 1') // 6expect(true).toEqual(true)})})console.log('describe outer-b') // 3test('test 1', () => {console.log('test for describe outer') // 7expect(true).toEqual(true)})describe('describe inner 2', () => {console.log('describe inner 2') // 4test('test for describe inner 2', () => {console.log('test for describe inner 2') // 8expect(false).toEqual(false)})})console.log('describe outer-c') // 5 }) 復制代碼Mocks
在測試中,mock 可以讓你更方便的去測試依賴于數據庫、網絡請求、文件等外部系統的函數。 Jest 內置了 mock 機制,提供了多種 mock 方式已應對各種需求。
Mock 函數
函數的 mock 非常簡單,調用 jest.fn() 即可獲得一個 mock 函數。 Mock 函數有一個特殊的 .mock 屬性,保存著函數的調用信息。.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 twiceexpect(mockCallback.mock.calls.length).toBe(2)// The first argument of the first call to the function was 0expect(mockCallback.mock.calls[0][0]).toBe(0)// The first argument of the second call to the function was 1expect(mockCallback.mock.calls[1][0]).toBe(1)// The return value of the first call to the function was 42expect(mockCallback.mock.results[0].value).toBe(42) }) 復制代碼除了 .mock 之外,Jest 還未我們提供了一些匹配器用來斷言函數的執行,它們本身只是檢查 .mock 屬性的語法糖:
// The mock function was called at least once expect(mockFunc).toBeCalled(); 復制代碼使用 mockReturnValue 和 mockReturnValueOnce 可以 mock 函數的返回值。 當我們需要為 mock 函數增加一些邏輯時,可以使用 jest.fn()、mockImplementation 或者 mockImplementationOnce mock 函數的實現。 還可以使用 mockName 還給 mock 函數命名,如果沒有命名,輸出的日志默認就會打印 jest.fn()。
Mock 定時器
Jest 可以 Mock 定時器以使我們在測試代碼中控制“時間”。調用 jest.useFakeTimers() 函數可以偽造定時器函數,定時器中的回調函數不會被執行,使用 setTimeout.mock 等可以斷言定時器執行情況。當在測試中有多個定時器時,執行 jest.useFakeTimers() 可以重置內部的計數器。
執行 jest.runAllTimers(); 可以“快進”直到所有的定時器被執行;執行 jest.runOnlyPendingTimers() 可以使當前正在等待的定時器被執行,用來處理定時器中設置定時器的場景,如果使用 runAllTimers 會導致死循環;執行 jest.advanceTimersByTime(msToRun:number),可以“快進”執行的毫秒數。
Mock 模塊
模塊的 mock 主要有兩種方式:
- 使用 jest.mock(moduleName, factory, options) 自動 mock 模塊,jest 會自動幫我們 mock 指定模塊中的函數。其中,factory 和 options 參數是可選的。factory 是一個模塊工廠函數,可以代替 Jest 的自動 mock 功能;options 用來創建一個不存在的需要模塊。
- 如果希望自己 mock 模塊內部函數,可以在模塊平級的目錄下創建 __mocks__ 目錄,然后創建相應模塊的 mock 文件。對于用戶模塊和 Node 核心模塊(如:fs、path),我們仍需要在測試文件中顯示的調用 jest.mock(),而其他的 Node 模塊則不需要。
此外,在 mock 模塊時,jest.mock() 會被自動提升到模塊導入前調用。
對于類的 mock 基本和模塊 mock 相同,支持自動 mock、手動 mock 以及調用帶模塊工廠參數的 jest.mock(),還可以調用 jest.mockImplementation() mock 構造函數。
快照測試
快照測試是 Jest 提供的一個相當棒的 UI 測試功能,它會記錄 React 結構樹快照或其他可序列化的值,并與當前測試的值進行比較,如果不匹配則給出錯誤提示。快照應該被當做代碼來對待,它需要被提交到版本庫并進行 Review。
如果組件渲染結果發生變化,測試將會失敗。當組件正常調整時,我們可以調用 jest -u 更新快照。在監控模式下,我們可以通過交互式的命令更新快照。
下面通過一個簡單的 text 組件來測試一下:
// Text.jsimport 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.jsimport 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() }) 復制代碼執行測試代碼后,會生成如下快照:
// Jest Snapshot v1, https://goo.gl/fbAQLPexports[`render correctly 1`] = ` <spanclassName="success" >Snapshot testing </span> `; 復制代碼如果后續修改導致組件渲染結果發生變化,快照將會不匹配,測試則不通過。
Jest 命令行
jest 命令行工具有有用的選項。運行 jest -h 可以查看所有可用的選項。所有的 Jest 的 配置項都可以通過命令行來指定。
基本用法:jest [--config=<pathToConfigFile>] [TestPathPattern] 生成配置信息:jest --init 運行符合指定用模板或文件名的測試︰jest path/to/my-test.js 啟動監視模式︰jest --watch 生成覆蓋率報告:jest --coverage
Jest 配置
Jest 的一個理念是提供一套完整集成的“零配置”測試體驗,開發人員可以直接上手編寫測試用例。它為我們集成了測試常用的工具,多數情況下使用默認配置或少量的調整即可。
Jest 的配置可以定義在 package.json 或 jest.config.js 文件中或通過命令行參數 --config <path/to/js|json>。配置并不是必須的,具體內容見文檔,按需取用即可。
PS:Jest 中 testURL 的默認值是 about:blank,在 jsdom 環境下運行會報錯,設置了 testURL 為一個有效的 URL 后能夠避免這個問題,如:http://localhost。
總結
以上是生活随笔為你收集整理的测试框架 Jest 实例教程的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: linux挂载windows共享的文件夹
- 下一篇: 用汇编实现add函数