Jest 是 Facebook 开源的一个 JavaScript 测试框架,已经应用在 Babel / TypeScript / Node / React / Angular / Vue 等项目中。
在愉悦地编写 Jest 测试代码后,你是否会好奇 test
/ expect
/ mock
/ spyOn
这些内置方法是如何实现的呢?下面我们一起来看一下。
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
函数的典型使用实例是:
// 需要测试的函数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 12test('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 来更好的管理。Jest 使用 jest.fn
是实现 Mock
// random.jsfunction getRandom(min, max) {return Math.floor(Math.random() * (max - min) + min)}export { getRandom }// cards.jsimport { getRandom } from './random.js'const getRandomCard = (cards) => {const randomCardIndex = getRandom(0, array.length)return cards[randomCardIndex]}export { getRandomCard }// cards.test.jsimport * as randomGenerator from './random.js'import { getRandomCard } from './cards.js'test('Returns 7♥', () => {const originalImplementation = randomGenerator.getRandomrandomGenerator.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 idempotentrandomGenerator.getRandom = originalImplementation})
为了实现上面的效果,我们可以这样模拟定义jest.fn
和 expect
// jest.fnfunction fn(impl) {const mockFn = (...args) => {mockFn.mock.calls.push(args)return impl(...args)}mockFn.mock = {calls: []}return mockFn}// expectimport 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 versionif (!assert.deepStrictEqual(current.mock.calls[0], ...params)) {throw new Error(`Expected: ${expected}Called: ${func.mock.calls.length}`)}}}}