iT邦幫忙

第 12 屆 iThome 鐵人賽

DAY 8
1
Modern Web

循序漸進學習 Javascript 測試系列 第 8

Day8 理解 Mock 基礎概念:使用 jest.spyOn 復原被 mock 的 function

到目前為止,我們還是「手動」地記住及復原需要被 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,接收 objectprop 兩個參數,加入 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 的功能,理解背後的原理。


上一篇
Day7 理解 Mock 基礎概念:初探 mock function,確保 Functions 被正確呼叫
下一篇
Day9 理解 Mock 基礎概念:mock 整個 module 及共用 mock module
系列文
循序漸進學習 Javascript 測試30

尚未有邦友留言

立即登入留言