原文:But really, what is a JavaScript mock?javascript
By Ken C. Doddshtml
刪減了前幾段吹牛逼的內容,直接進入正題java
要想知道mock是啥,首先得有東西讓你去測、去mock,下面是咱們要測試的代碼:react
import {getWinner} from './utils'
function thumbWar(player1, player2) {
const numberToWin = 2
let player1Wins = 0
let player2Wins = 0
while (player1Wins < numberToWin && player2Wins < numberToWin) {
const winner = getWinner(player1, player2)
if (winner === player1) {
player1Wins++
} else if (winner === player2) {
player2Wins++
}
}
return player1Wins > player2Wins ? player1 : player2
}
export default thumbWar
複製代碼
這是一個猜拳遊戲,三局兩勝。從utils
庫中使用了一個叫getWinner
的函數。這個函數返回獲勝的人,若是是平局則返回null
。咱們假設getWinner
是調用了某個第三方的機器學習服務,也就是說咱們的測試環境沒法控制它,因此咱們須要在測試中mock一下。這是一種你只能經過mock才能可靠地測試你的代碼的情景。(這裏爲了簡化,假設這個函數是同步的)git
另外,除了從新實現一遍getWinner
的邏輯,咱們實際上不太可能作出有用的判斷以肯定猜拳遊戲中究竟是誰獲勝了。因此,沒有mocking的狀況下,下面就是咱們能給出的最好的測試了:github
譯註:沒有mocking的狀況下,只能斷言獲勝的選手是參賽選手的一個,這幾乎沒什麼用api
import thumbWar from '../thumb-war'
test('returns winner', () => {
const winner = thumbWar('Ken Wheeler', 'Kent C. Dodds')
expect(['Ken Wheeler', 'Kent C. Dodds'].includes(winner)).toBe(true)
})
複製代碼
Mocking最簡單的形式是一種稱做猴子補丁(Monkey-patching)的形式。下面給出一個例子:緩存
譯註:猴子補丁是指在本地修改引入的代碼,可是隻能對當前運行的實例有影響。bash
import thumbWar from '../thumb-war'
import * as utils from '../utils'
test('returns winner', () => {
const originalGetWinner = utils.getWinner
// eslint-disable-next-line import/namespace
utils.getWinner = (p1, p2) => p2
const winner = thumbWar('Ken Wheeler', 'Kent C. Dodds')
expect(winner).toBe('Kent C. Dodds')
// eslint-disable-next-line import/namespace
utils.getWinner = originalGetWinner
})
複製代碼
看上面的代碼,你能夠注意到如下幾點:一、咱們必須採用import * as
的形式引入utils
,以便於接下來能夠操做這個對象(後面會談到,這種形式有啥壞處)。二、咱們須要先把要mock的函數原始值保存起來,而後在測試後恢復原來的值,這樣其餘用到utils
的測試才能不受這個測試用例的影響。機器學習
上面的全部操做都是爲了咱們可以mock getWinner
函數,而實際上的mock操做只有一行代碼:
utils.getWinner = (p1, p2) => p2
複製代碼
這就是所謂的猴子補丁,目前來看它是有效的(咱們如今可以肯定猜拳遊戲中一個肯定的勝者了),可是仍然有不少不足。首先,讓咱們感到噁心的是這些eslint warning,因此咱們加入了不少eslint-disable
(再次強調,不要在你的代碼中這麼搞,後面咱們還會提到它)。第二,咱們仍然不知道getWinner
函數是否調用了咱們指望它被調用的次數(2次,三局兩勝嘛)。對於咱們的應用來講,這也許是不重要的,但對於本文要講的mock來講是很重要的。因此,接下來咱們來優化它。
接下來咱們增長一些代碼,以肯定getWinner
函數被調用了兩次,而且確認每次調用的時候,都傳入了正確的參數。
import thumbWar from '../thumb-war'
import * as utils from '../utils'
test('returns winner', () => {
const originalGetWinner = utils.getWinner
// eslint-disable-next-line import/namespace
utils.getWinner = (...args) => {
utils.getWinner.mock.calls.push(args)
return args[1]
}
utils.getWinner.mock = {calls: []}
const winner = thumbWar('Ken Wheeler', 'Kent C. Dodds')
expect(winner).toBe('Kent C. Dodds')
expect(utils.getWinner.mock.calls).toHaveLength(2)
utils.getWinner.mock.calls.forEach(args => {
expect(args).toEqual(['Ken Wheeler', 'Kent C. Dodds'])
})
// eslint-disable-next-line import/namespace
utils.getWinner = originalGetWinner
})
複製代碼
上面的代碼咱們加入了一個mock
對象,用以保存被mock函數在被調用時產生的一些元數據。有了它,咱們能夠給出下面兩個斷言:
expect(utils.getWinner.mock.calls).toHaveLength(2)
utils.getWinner.mock.calls.forEach(args => {
expect(args).toEqual(['Ken Wheeler', 'Kent C. Dodds'])
})
複製代碼
這兩個斷言確保咱們的mock函數被適當地調用了(傳入了正確的參數),而且調用的次數也正確(對於三局兩勝來講就是2次)。
既然如今咱們的mock能夠提現真實運行的情景,咱們能夠對咱們的代碼(thumbWar
)更有信息了。可是很差的一點是,咱們必需要給出這個mock函數到底在作啥。TODO
目前爲止,一切都好,但噁心的是咱們必需要手動加入追蹤邏輯以記錄mock函數的調用信息。Jest內置了這種mock功能,接下來咱們使用Jest簡化咱們的代碼:
import thumbWar from '../thumb-war'
import * as utils from '../utils'
test('returns winner', () => {
const originalGetWinner = utils.getWinner
// eslint-disable-next-line import/namespace
utils.getWinner = jest.fn((p1, p2) => p2)
const winner = thumbWar('Ken Wheeler', 'Kent C. Dodds')
expect(winner).toBe('Kent C. Dodds')
expect(utils.getWinner).toHaveBeenCalledTimes(2)
utils.getWinner.mock.calls.forEach(args => {
expect(args).toEqual(['Ken Wheeler', 'Kent C. Dodds'])
})
// eslint-disable-next-line import/namespace
utils.getWinner = originalGetWinner
})
複製代碼
這裏咱們只是使用jest.fn
把getWinner
的mock函數包起來了。基本功能跟咱們以前本身實現的mock差很少,可是使用Jest的mock,咱們可使用一些Jest提供的指定斷言(好比toHaveBeenCalledTines
),顯然更方便。不幸的是,Jest並無提供相似nthCalledWidth
(好像快要支持了)這樣的API,不然咱們就能夠避免這些forEach
語句了。但即便這樣,一切看起來尚好。
另一件我不喜歡的事是要手動保存originalGetWinner
,而後在測試結束後恢復原狀。還要那些煩人的eslint註釋(這很重要,咱們一下子會專門說這個)。接下來,咱們看一下咱們能不能用Jest提供的工具把咱們的代碼進一步簡化。
幸運的是,Jest有一個工具函數叫spyOn
,提供了咱們所需的功能。
import thumbWar from '../thumb-war'
import * as utils from '../utils'
test('returns winner', () => {
jest.spyOn(utils, 'getWinner')
utils.getWinner.mockImplementation((p1, p2) => p2)
const winner = thumbWar('Ken Wheeler', 'Kent C. Dodds')
expect(winner).toBe('Kent C. Dodds')
utils.getWinner.mockRestore()
})
複製代碼
不錯,代碼確實簡單了很多。Mock函數又被叫作spy(這也是爲啥這個API叫spyOn
)。默認Jest會保存getWinner
的原始實現,而且追蹤它是如何被調用的。咱們不但願原始的實現被調用,因此咱們用mockImplementation
去指定咱們調用它時應該返回什麼結果。最後,咱們再用mockRestore
去清除mock操做,以保留getWinner
原本的與昂子。(跟咱們以前所作的同樣,對吧)。
還記得以前咱們提到的eslint error嗎,咱們接下來解決這個問題。
咱們遇到的ESLint報錯很是重要。咱們之因此會遇到這個問題,是由於咱們寫代碼的方式致使eslint-plugin-import
不能靜態檢測咱們是否破壞了它的規則。這個規則很是重要,就是:import/namespace
。之因此咱們會破壞這個規則是由於對import命名空間的成員進行了賦值。
爲啥這會是個問題呢?由於咱們的ES6代碼被Babel轉成了CommonJS的形式,而CommonJS中有所謂的require緩存。當我import 一個模塊時,我其實是在import哪一個模塊中函數的執行環境。因此當我在不一樣的文件引入相同的模塊,並嘗試去修改這個執行環境,這個修改僅對當前文件有效。因此若是你很依賴這個特性,你極可能在升級ES6模塊時遇到坑。
Jest模擬了一套模塊系統,從而能夠很是容易的無縫將咱們的mock實現替換掉原始實現,如今咱們的測試變成了這個樣子:
import thumbWar from '../thumb-war'
import * as utilsMock from '../utils'
jest.mock('../utils', () => {
return {
getWinner: jest.fn((p1, p2) => p2),
}
})
test('returns winner', () => {
const winner = thumbWar('Ken Wheeler', 'Kent C. Dodds')
expect(winner).toBe('Kent C. Dodds')
expect(utilsMock.getWinner).toHaveBeenCalledTimes(2)
utilsMock.getWinner.mock.calls.forEach(args => {
expect(args).toEqual(['Ken Wheeler', 'Kent C. Dodds'])
})
})
複製代碼
咱們直接告訴Jest咱們但願全部的文件去使用咱們的mock版本。注意我修改了import過來的名字爲utilsMock
。這不是必須的,可是我喜歡用這種方式代表這裏import過來的是個mock版本而非原始實現。
常見問題:若是你想要僅mock某個模塊中的一個函數,也許你想看看
require.requireActual
API
到這裏就幾乎快要說完了。假如咱們要在多個測試中用到getWinner
函數,可是又不想處處複製粘貼這段mock代碼怎麼辦?這就須要用到__mocks__
文件夾提供方便了。因此咱們在咱們想要對其mock的文件旁邊建立一個__mocks__
文件夾,而後建立一個相同名字的文件:
other/whats-a-mock/
├── __mocks__
│ └── utils.js
├── __tests__/
├── thumb-war.js
└── utils.js
複製代碼
在__mocks__/utils.js
文件中,咱們這麼寫:
// __mocks__/utils.js
export const getWinner = jest.fn((p1, p2) => p2)
複製代碼
這樣咱們的測試能夠寫成:
// __tests__/thumb-war.js
import thumbWar from '../thumb-war'
import * as utilsMock from '../utils'
jest.mock('../utils')
test('returns winner', () => {
const winner = thumbWar('Ken Wheeler', 'Kent C. Dodds')
expect(winner).toBe('Kent C. Dodds')
expect(utilsMock.getWinner).toHaveBeenCalledTimes(2)
utilsMock.getWinner.mock.calls.forEach(args => {
expect(args).toEqual(['Ken Wheeler', 'Kent C. Dodds'])
})
})
複製代碼
如今咱們只須要寫jest.mock(pathToModule)
就能夠了,它會自動使用咱們剛纔建立的mock實現。
咱們也許不想mock實現老是返回第二個選手獲勝,這時咱們就能夠針對特定的測試用mockImplementation
給出指望的實現,進而測試其餘狀況是否測試經過。你也能夠在你的mock中使用一些工具庫方法,想怎麼玩兒都行。
End.