iT邦幫忙

第 12 屆 iThome 鐵人賽

DAY 7
1
Modern Web

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

Day7 理解 Mock 基礎概念:初探 mock function,確保 Functions 被正確呼叫

通常,在測試 JavaScript 跟 Mock 依賴的時候,我們需要確認 Functions 是不是有被正確地呼叫,例如:被呼叫了幾次?參數是否有正常傳遞進去?

使用 Jest 提供的 jest.fn 方法

前面我們在 monkey-patching.js 這個檔案裡面只是單純將 utils.getWinner 改為另一個 function,但如果我們要做到能測試 function 是否有被正確呼叫的話,這樣是有限的。
Jest 提供了一個 build-in 方法 jest.fn ,你可以傳入一個 function 給 jest.fn ,回傳的就叫做 mock function,Jest 爲 mock function 提供一些 API,方便我們測試追蹤 function 是否有被正確呼叫

utils.getWinner = jest.fn((p1, p2) => p1)

假設我們現在用 Jest 來寫測試,有一個資料夾 __test__ 裡面有一個測試檔案 mock-fn.js , 試著來寫一個測試,除了驗證 thumbWar 結果之外,也要驗證 utils.getWinner 是否有被正確呼叫:

test/mock-fn.js

const thumbWar = require('../thumb-war')
const utils = require('../utils')

test('returns winner', () => {
  const originalGetWinner = utils.getWinner
  utils.getWinner = jest.fn((p1, p2) => p1) // mock function

  const winner = thumbWar('小夫', '胖虎')
  expect(winner).toBe('小夫')
  expect(utils.getWinner).toHaveBeenCalledTimes(2) // utils.getWinner 被呼叫兩次
  expect(utils.getWinner).toHaveBeenCalledWith('小夫', '胖虎') // utils.getWinner 是否傳入正確的參數
	expect(utils.getWinner).toHaveBeenNthCalledWith(
    1,
    '小夫',
    '胖虎'
  ) // utils.getWinner 第一次呼叫傳入的參數
  expect(utils.getWinner).toHaveBeenNthCalledWith(
    2,
    '小夫',
    '胖虎'
  ) // utils.getWinner 第二次呼叫傳入的參數

	utils.getWinner = originalGetWinner
})

關於 Jest 所提供的方法,詳細可參考 官方文件

剛剛提到 Jest 有提供 mock function 一些實用的 API,例如: mockFn.mock.calls ,它提供一個 array,裡面包含了 mock function 每次被呼叫的參數,現在我們用這個 API 讓測試程式碼更簡潔:

test/mock-fn.js

test('returns winner', () => {
  const originalGetWinner = utils.getWinner
  utils.getWinner = jest.fn((p1, p2) => p1) // mock function

  const winner = thumbWar('小夫', '胖虎')
  expect(winner).toBe('小夫')
  expect(utils.getWinner.mock.calls).toEqual([
	  [ '小夫', '胖虎' ],
	  [ '小夫', '胖虎' ]
	]) // 現在只需要這樣

	utils.getWinner = originalGetWinner
})

自己動手造輪子

為了更理解工具的原理,現在我們來試試不用 Jest,自己做一個跟 jest.fn 差不多的功能。

no-framework 資料夾中,創建一個 mock-fn.js 檔案,宣告一個叫做 fn 的 function,這個 function 跟 jest.fn 一樣可以 return 一個 mock function:

no-framework/mock-fn.js

function fn(impl) {
  const mockFn = (...args) => {
    return impl(...args)
  }
  return mockFn
}

接下來,我們要像 Jest 的 mock function API 一樣,提供一個屬性 mock.calls,透過這個屬性取得每次呼叫時傳入的參數:

no-framework/mock-fn.js

function fn(impl) {
  const mockFn = (...args) => {
    mockFn.mock.calls.push(args)
    return impl(...args)
  }
  mockFn.mock = {calls: []}
  return mockFn
}

最後,我們不用 Jest 所寫的測試,如下:

no-framework/mock-fn.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: []}
  return mockFn
}

const originalGetWinner = utils.getWinner
utils.getWinner = fn((p1, p2) => p1)

const winner = thumbWar('小夫', '胖虎')
assert.strictEqual(winner, '小夫')
assert.deepStrictEqual(utils.getWinner.mock.calls, [
  ['小夫', '胖虎'],
  ['小夫', '胖虎']
])

utils.getWinner = originalGetWinner

小結

今天我們學習如何測試 function 是否被正確地呼叫。透過 Jest 的 jest.fn 回傳 mock function,以及 mock function 裡的 mock.calls 屬性,驗證呼叫次數、每次呼叫所傳入的參數。最後,動手實作一個簡易的 function fn,模仿 jest.fn 的功能,理解背後的原理。


上一篇
Day6 理解 Mock 基礎概念:從 Monkey-patching 開始
下一篇
Day8 理解 Mock 基礎概念:使用 jest.spyOn 復原被 mock 的 function
系列文
循序漸進學習 Javascript 測試30

尚未有邦友留言

立即登入留言