Jest 内部原理剖析

Jest 是 Facebook 开源的一个 JavaScript 测试框架,已经应用在 Babel / TypeScript / Node / React / Angular / Vue 等项目中。

在愉悦地编写 Jest 测试代码后,你是否会好奇 test / expect / mock / spyOn 这些内置方法是如何实现的呢?下面我们一起来看一下。

Test

Jest 中的 test 函数标准实例是:

test('Passing test', () => {})
// ✓ Passing test
test('Failing test', () => new Error('Error message'))
// ✕ Failing test
// Error message

这里可以注意到,如果第二个参数的函数执行没有抛出异常的话,会打印第一个参数,因此我们的模拟实现可以是:

function test(title, callback) {
  try {
    callback()
    console.log(`✓ ${title}`)
  } catch (error) {
    console.error(`✕ ${title}`)
    console.error(error)
  }
}

Expect

expect 函数的典型使用实例是:

// 需要测试的函数
function multiply(a, b) {
   return a * b
}

// 测试用例
test('Multipling 3 by 4 is 12', () => {
   expect(multiply(3, 4)).toBe(12)
} // ✓ Multipling 3 by 4 is 12

test('Multipling 3 by 4 is 12', () => {
   expect(multiply(3, 4)).toBe(13)
}) // ✕ Multipling 3 by 4 is 12
   // Expected: 13
   // Received: 12

注意到expect 的返回值具备一个可以调用的方法toBe ,那么我们可以这样模拟expect

function expect(current) {
  return {
    toBe(expected) {
      if (current !== expected) {
        throw new Error(`
          Expected: ${expected}
          Received: ${current}
        `)
      }
    }
  }
}

Mock

当我们要避免测试一个功能时又依赖于不稳定的另外一个功能,我们创建一个 Mock 来更好的管理。Jest 使用 jest.fn 是实现 Mock

// random.js
function getRandom(min, max) {
    return Math.floor(Math.random() * (max - min) + min)
}
export { getRandom }

// cards.js
import { getRandom } from './random.js'
const getRandomCard = (cards) => {
   const randomCardIndex = getRandom(0, array.length)
   return cards[randomCardIndex]
}
export { getRandomCard }

// cards.test.js
import * as randomGenerator from './random.js'
import { getRandomCard } from './cards.js'
test('Returns 7♥', () => {
    const originalImplementation = randomGenerator.getRandom
    randomGenerator.getRandom = jest.fn(() => 2)
    const result = getRandomCard(['2♣', 'K♦️', '7♥', '3♠'])
    expect(result).toBe('7♥')
    expect(randomGenerator.getRandom).toHaveBeenCalledTimes(1)
    expect(randomGenerator.getRandom).toHaveBeenCalledWith(0, 4)
    // we keep the test idempotent
    randomGenerator.getRandom = originalImplementation
})

为了实现上面的效果,我们可以这样模拟定义jest.fnexpect

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

// expect
import assert from 'assert'
function expect(current) {
  return {
    toHaveBeenCalledTimes(nrTimesExpected) {
      if (current.mock.calls.length !== nrTimesExpected) {
        throw new Error(`
          Expected: ${expected}
          Called: ${func.mock.calls.length}
        `)
      }
    },
    toHaveBeenCalledWith(...params) {
        // this is a simplified version         
        if (!assert.deepStrictEqual(current.mock.calls[0], ...params)) {
            throw new Error(`
               Expected: ${expected}
               Called: ${func.mock.calls.length}
            `)
        }
    }
  }
}

最后更新于