測試框架 Jest 實例教程

Jest 是由 Facebook 開源出來的一個測試框架,它集成了斷言庫、mock、快照測試、覆蓋率報告等功能。它很是適合用來測試 React 代碼,但不只僅如此,全部的 js 代碼均可以使用 Jest 進行測試。node

本文全面的介紹如何使用 Jest,讓後來者輕鬆上手。文中會選取重點部分直接貼出代碼,比較簡單的部分則不會,主要是寫到後面的時候發現貼的代碼有點多,沒什麼意思,全部的代碼已上傳到 Github,能夠自行查閱。react

安裝

使用 yarn 安裝 Jest:webpack

$ yarn add --dev jest
複製代碼

或使用 npmgit

$ npm i -D jest
複製代碼

其中 --dev-D 參數指明做爲 devDependencies,這樣該依賴只會在開發環境下安裝,在生成環境下則不會。github

package.json 文件中添加下面的內容:web

"scripts": {
  "test": "jest"
}
複製代碼

這樣咱們就能夠經過 yarn testnpm test 執行測試代碼。數據庫

一樣地,你也能夠選擇全局安裝 Jest:npm

$ yarn global add jest
$ # or npm i -g jest
複製代碼

這樣你就能夠直接在命令行使用 jest 命令。若是你是本地安裝,可是也想在命令行使用 jest,能夠經過 node_modules/.bin/webpack 訪問它的 bin 版本,若是你的 npm 版本在 5.2.0 以上,你也能夠經過 npx jest 訪問。json

使用 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.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。咱們這裏則建立一個 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)
})
複製代碼

執行 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 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 須要知道何時異步代碼執行完成,在異步代碼執行完以前,它會去執行其餘的測試代碼。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')) // 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 以前執行。

Jest 會先執行 describe 塊內的操做,等 describe 塊內的操做執行完畢後,按照出如今 describe 中的前後順序執行測試用例,所以初始化和銷燬操做應該放在鉤子函數中運行,而不是 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 可讓你更方便的去測試依賴於數據庫、網絡請求、文件等外部系統的函數。 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 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 還未咱們提供了一些匹配器用來斷言函數的執行,它們自己只是檢查 .mock 屬性的語法糖:

// The mock function was called at least once
expect(mockFunc).toBeCalled();
複製代碼

使用 mockReturnValuemockReturnValueOnce 能夠 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 指定模塊中的函數。其中,factoryoptions 參數是可選的。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.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()
})
複製代碼

執行測試代碼後,會生成以下快照:

// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`render correctly 1`] = `
<span
  className="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.jsonjest.config.js 文件中或經過命令行參數 --config <path/to/js|json>。配置並非必須的,具體內容見文檔,按需取用便可。

PS:Jest 中 testURL 的默認值是 about:blank,在 jsdom 環境下運行會報錯,設置了 testURL 爲一個有效的 URL 後可以避免這個問題,如:http://localhost

相關文章
相關標籤/搜索