測試框架 Jest 實例教程

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 testnpm 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í)行所有匹配的測試文件子库,并最終返回測試結果:

basic-result.png

在編輯器中運行

很多編輯器都能支持 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ā)生修改后重新運行雷袋。

vscode-jest.png

匹配器

匹配器用來實現(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();

使用 mockReturnValuemockReturnValueOnce 可以 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ù)。其中攻柠,factoryoptions 參數(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)控模式下奋献,我們可以通過交互式的命令更新快照健霹。

interactiveSnapshot.png

下面通過一個簡單的 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ā)生變化,快照將會不匹配瞳别,測試則不通過征候。

snapshot_test_fail.png

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.jsonjest.config.js 文件中或通過命令行參數(shù) --config <path/to/js|json>叼架。配置并不是必須的畔裕,具體內容見文檔,按需取用即可乖订。

PS:Jest 中 testURL 的默認值是 about:blank扮饶,在 jsdom 環(huán)境下運行會報錯,設置了 testURL 為一個有效的 URL 后能夠避免這個問題乍构,如:http://localhost甜无。

?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市哥遮,隨后出現(xiàn)的幾起案子岂丘,更是在濱河造成了極大的恐慌,老刑警劉巖眠饮,帶你破解...
    沈念sama閱讀 216,692評論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件奥帘,死亡現(xiàn)場離奇詭異,居然都是意外死亡仪召,警方通過查閱死者的電腦和手機寨蹋,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,482評論 3 392
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來扔茅,“玉大人已旧,你說我怎么就攤上這事≌倌龋” “怎么了运褪?”我有些...
    開封第一講書人閱讀 162,995評論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我吐句,道長胁后,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,223評論 1 292
  • 正文 為了忘掉前任嗦枢,我火速辦了婚禮,結果婚禮上屯断,老公的妹妹穿的比我還像新娘文虏。我一直安慰自己,他們只是感情好殖演,可當我...
    茶點故事閱讀 67,245評論 6 388
  • 文/花漫 我一把揭開白布氧秘。 她就那樣靜靜地躺著,像睡著了一般趴久。 火紅的嫁衣襯著肌膚如雪丸相。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,208評論 1 299
  • 那天彼棍,我揣著相機與錄音灭忠,去河邊找鬼弛作。 笑死映琳,一個胖子當著我的面吹牛蜘拉,可吹牛的內容都是我干的旭旭。 我是一名探鬼主播您机,決...
    沈念sama閱讀 40,091評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼咸产,長吁一口氣:“原來是場噩夢啊……” “哼仲闽!你這毒婦竟也來了?” 一聲冷哼從身側響起验庙,我...
    開封第一講書人閱讀 38,929評論 0 274
  • 序言:老撾萬榮一對情侶失蹤粪薛,失蹤者是張志新(化名)和其女友劉穎搏恤,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體藤巢,經(jīng)...
    沈念sama閱讀 45,346評論 1 311
  • 正文 獨居荒郊野嶺守林人離奇死亡掂咒,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 37,570評論 2 333
  • 正文 我和宋清朗相戀三年绍刮,在試婚紗的時候發(fā)現(xiàn)自己被綠了录淡。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片嫉戚。...
    茶點故事閱讀 39,739評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖窍帝,靈堂內的尸體忽然破棺而出坤学,到底是詐尸還是另有隱情报慕,我是刑警寧澤眠冈,帶...
    沈念sama閱讀 35,437評論 5 344
  • 正文 年R本政府宣布飞苇,位于F島的核電站,受9級特大地震影響布卡,放射性物質發(fā)生泄漏雨让。R本人自食惡果不足惜忿等,卻給世界環(huán)境...
    茶點故事閱讀 41,037評論 3 326
  • 文/蒙蒙 一栖忠、第九天 我趴在偏房一處隱蔽的房頂上張望贸街。 院中可真熱鬧娃闲,春花似錦、人聲如沸匾浪。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,677評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春栋齿,著一層夾襖步出監(jiān)牢的瞬間苗胀,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,833評論 1 269
  • 我被黑心中介騙來泰國打工菇用, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留澜驮,地道東北人。 一個月前我還...
    沈念sama閱讀 47,760評論 2 369
  • 正文 我出身青樓惋鸥,卻偏偏與公主長得像杂穷,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子卦绣,可洞房花燭夜當晚...
    茶點故事閱讀 44,647評論 2 354

推薦閱讀更多精彩內容