到目前為止,我們還是「手動」地記住及復原需要被 mock 的 function,例如:
test('returns winner', () => {
const originalGetWinner = utils.getWinner
utils.getWinner = jest.fn((p1, p2) => p1)
...
utils.getWinner = originalGetWinner
})
但這樣其實滿不方便的,而 Jest 提供的 jest.spyOn 可以幫我們避免必須「手動」復原的情況。jest.spyOn 接收兩個參數,第一個是模組,第二個是 method 的名稱。
現在在 __test__ 資料夾新建一個檔案叫做 spy.js ,將 昨日文章 所寫的測試 __test__/mock-fn.js 內容複製到新檔案內,使用 jest.spyOn 來改寫。
jest.spyOn 會將 utils module 裡的 getWinner method 置換成一個「空的」mock function。我們不再需要 originalGetWinner 來記住原本的 function,而且搭配 jest.spyOn 來創建 mock function,可以使用 mockFn.mockRestore 這個 method 來復原回原本的 function:
test/spy.js
test('returns winner', () => {
jest.spyOn(utils, 'getWinner') // utils.getWinner 被取代成一個空的 mock function
utils.getWinner = jest.fn((p1, p2) => p2)
...
utils.getWinner.mockRestore() // 復原回原本的 function
})
Jest 的 Mock function 有一個 method 叫做 mockFn.mockImplementation,前面所使用過的 jest.fn(fn) 就是 jest.fn().mockImplementation(fn) 的簡單表示,現在我們使用 mockImplementation 來取代 jest.fn(fn):
test/spy.js
test('returns winner', () => {
jest.spyOn(utils, 'getWinner')
utils.getWinner.mockImplementation((p1, p2) => p2) // 改為使用 mockImplementation
...
utils.getWinner.mockRestore()
})
根據文件:
mockFn.mockRestore只有在 mock 是由jest.spyOn創建才有作用。另外,如果只有單純使用jest.fn()來創建 mock function 的話,只能「手動」復原。
為了更理解工具的原理,現在我們來試試不用 Jest,自己做一個跟 **jest.spyOn** 差不多的功能。
現在在 no-framework 資料夾新建一個檔案叫做 spy.js ,將 昨日文章 所寫的測試 no-framework/mock-fn.js 內容複製到新檔案內來改寫。
現在新增一個 function 叫 spyOn,接收 object 跟 prop 兩個參數,加入 const originalValue = obj[prop] 記住原始的 function,然後將 obj[prop] 設為呼叫 fn() 所回傳的「空的」 mock function。
no-framework/spy.js
function spyOn(obj, prop) {
const originalValue = obj[prop] // 記住原始的 function
obj[prop] = fn() // mock function
}
還記得昨天寫的 fn function 嗎?我們需要給它一個預設的參數值,一個空的 arrow function () => {} :
no-framework/spy.js
function fn(impl = () => {}) { // 設置預設的參數值為空的 arrow function
const mockFn = (...args) => {
mockFn.mock.calls.push(args)
return impl(...args)
}
mockFn.mock = {calls: []}
return mockFn
}
現在我們來實作用 mockRestore 復原 mock function 的部分。為 obj[prop] 新增一個 method 叫 mockRestore ,賦予 () => (obj[prop] = originalValue) :
no-framework/spy.js
function spyOn(obj, prop) {
const originalValue = obj[prop]
obj[prop] = fn()
obj[prop].mockRestore = () => (obj[prop] = originalValue) // 復原 mock function
}
目前我們透過 spyOn 裡面呼叫 fn() 所回傳的 mock function 是「空的」arrow function,接下來要為 fn function 加入一個 method mockImplementation ,讓我們可以透過它傳入 mock function:
no-framework/spy.js
function fn(impl = () => {}) {
const mockFn = (...args) => {
mockFn.mock.calls.push(args)
return impl(...args)
}
mockFn.mock = {calls: []}
mockFn.mockImplementation = newImpl => (impl = newImpl) // mock function
return mockFn
}
最後,我們不用 Jest 所寫的測試,如下:
no-framework/spy.js
const assert = require('assert')
const thumbWar = require('../thumb-war')
const utils = require('../utils')
function fn(impl = () => {}) {
const mockFn = (...args) => {
mockFn.mock.calls.push(args)
return impl(...args)
}
mockFn.mock = {calls: []}
mockFn.mockImplementation = newImpl => (impl = newImpl)
return mockFn
}
function spyOn(obj, prop) {
const originalValue = obj[prop]
obj[prop] = fn()
obj[prop].mockRestore = () => (obj[prop] = originalValue)
}
spyOn(utils, 'getWinner')
utils.getWinner.mockImplementation((p1, p2) => p1)
const winner = thumbWar('小夫', '胖虎')
assert.strictEqual(winner, '小夫')
assert.deepStrictEqual(utils.getWinner.mock.calls, [
['小夫', '胖虎'],
['小夫', '胖虎']
])
// cleanup
utils.getWinner.mockRestore()
Jest 提供的 jest.spyOn 可以幫我們 mock function 時,更方便地處理記住原始 function,並且用 mockRestore 來復原原始 function。另外,搭配 mockFn.mockImplementation method 來 mock function。最後,動手實作一個簡易的 function spyOn,模仿 jest.spyOn 的功能,理解背後的原理。