到底啥是JavaScript Mock

原文:But really, what is a JavaScript mock?javascript

By Ken C. Doddshtml

删减了前几段吹牛逼的内容,直接进入正题java

第0步

要想知道mock是啥,首先得有东西让你去测、去mock,下面是咱们要测试的代码:react

import {getWinner} from './utils'
function thumbWar(player1, player2) {
  const numberToWin = 2
  let player1Wins = 0
  let player2Wins = 0
  while (player1Wins < numberToWin && player2Wins < numberToWin) {
    const winner = getWinner(player1, player2)
    if (winner === player1) {
      player1Wins++
    } else if (winner === player2) {
      player2Wins++
    }
  }
  return player1Wins > player2Wins ? player1 : player2
}
export default thumbWar
复制代码

这是一个猜拳游戏,三局两胜。从utils库中使用了一个叫getWinner的函数。这个函数返回获胜的人,若是是平局则返回null。咱们假设getWinner是调用了某个第三方的机器学习服务,也就是说咱们的测试环境没法控制它,因此咱们须要在测试中mock一下。这是一种你只能经过mock才能可靠地测试你的代码的情景。(这里为了简化,假设这个函数是同步的)git

另外,除了从新实现一遍getWinner的逻辑,咱们实际上不太可能作出有用的判断以肯定猜拳游戏中究竟是谁获胜了。因此,没有mocking的状况下,下面就是咱们能给出的最好的测试了:github

译注:没有mocking的状况下,只能断言获胜的选手是参赛选手的一个,这几乎没什么用api

import thumbWar from '../thumb-war'
test('returns winner', () => {
  const winner = thumbWar('Ken Wheeler', 'Kent C. Dodds')
  expect(['Ken Wheeler', 'Kent C. Dodds'].includes(winner)).toBe(true)
})
复制代码

第1步

Mocking最简单的形式是一种称做猴子补丁(Monkey-patching)的形式。下面给出一个例子:缓存

译注:猴子补丁是指在本地修改引入的代码,可是只能对当前运行的实例有影响。bash

import thumbWar from '../thumb-war'
import * as utils from '../utils'
test('returns winner', () => {
  const originalGetWinner = utils.getWinner
  // eslint-disable-next-line import/namespace
  utils.getWinner = (p1, p2) => p2
  const winner = thumbWar('Ken Wheeler', 'Kent C. Dodds')
  expect(winner).toBe('Kent C. Dodds')
  // eslint-disable-next-line import/namespace
  utils.getWinner = originalGetWinner
})
复制代码

看上面的代码,你能够注意到如下几点:一、咱们必须采用import * as的形式引入utils,以便于接下来能够操做这个对象(后面会谈到,这种形式有啥坏处)。二、咱们须要先把要mock的函数原始值保存起来,而后在测试后恢复原来的值,这样其余用到utils的测试才能不受这个测试用例的影响。机器学习

上面的全部操做都是为了咱们可以mock getWinner函数,而实际上的mock操做只有一行代码:

utils.getWinner = (p1, p2) => p2
复制代码

这就是所谓的猴子补丁,目前来看它是有效的(咱们如今可以肯定猜拳游戏中一个肯定的胜者了),可是仍然有不少不足。首先,让咱们感到恶心的是这些eslint warning,因此咱们加入了不少eslint-disable(再次强调,不要在你的代码中这么搞,后面咱们还会提到它)。第二,咱们仍然不知道getWinner函数是否调用了咱们指望它被调用的次数(2次,三局两胜嘛)。对于咱们的应用来讲,这也许是不重要的,但对于本文要讲的mock来讲是很重要的。因此,接下来咱们来优化它。

第2步

接下来咱们增长一些代码,以肯定getWinner函数被调用了两次,而且确认每次调用的时候,都传入了正确的参数。

import thumbWar from '../thumb-war'
import * as utils from '../utils'
test('returns winner', () => {
  const originalGetWinner = utils.getWinner
  // eslint-disable-next-line import/namespace
  utils.getWinner = (...args) => {
    utils.getWinner.mock.calls.push(args)
    return args[1]
  }
  utils.getWinner.mock = {calls: []}
  const winner = thumbWar('Ken Wheeler', 'Kent C. Dodds')
  expect(winner).toBe('Kent C. Dodds')
  expect(utils.getWinner.mock.calls).toHaveLength(2)
  utils.getWinner.mock.calls.forEach(args => {
    expect(args).toEqual(['Ken Wheeler', 'Kent C. Dodds'])
  })
  // eslint-disable-next-line import/namespace
  utils.getWinner = originalGetWinner
})
复制代码

上面的代码咱们加入了一个mock对象,用以保存被mock函数在被调用时产生的一些元数据。有了它,咱们能够给出下面两个断言:

expect(utils.getWinner.mock.calls).toHaveLength(2)
utils.getWinner.mock.calls.forEach(args => {
  expect(args).toEqual(['Ken Wheeler', 'Kent C. Dodds'])
})
复制代码

这两个断言确保咱们的mock函数被适当地调用了(传入了正确的参数),而且调用的次数也正确(对于三局两胜来讲就是2次)。

既然如今咱们的mock能够提现真实运行的情景,咱们能够对咱们的代码(thumbWar)更有信息了。可是很差的一点是,咱们必需要给出这个mock函数到底在作啥。TODO

第3步

目前为止,一切都好,但恶心的是咱们必需要手动加入追踪逻辑以记录mock函数的调用信息。Jest内置了这种mock功能,接下来咱们使用Jest简化咱们的代码:

import thumbWar from '../thumb-war'
import * as utils from '../utils'
test('returns winner', () => {
  const originalGetWinner = utils.getWinner
  // eslint-disable-next-line import/namespace
  utils.getWinner = jest.fn((p1, p2) => p2)
  const winner = thumbWar('Ken Wheeler', 'Kent C. Dodds')
  expect(winner).toBe('Kent C. Dodds')
  expect(utils.getWinner).toHaveBeenCalledTimes(2)
  utils.getWinner.mock.calls.forEach(args => {
    expect(args).toEqual(['Ken Wheeler', 'Kent C. Dodds'])
  })
  // eslint-disable-next-line import/namespace
  utils.getWinner = originalGetWinner
})
复制代码

这里咱们只是使用jest.fngetWinner的mock函数包起来了。基本功能跟咱们以前本身实现的mock差很少,可是使用Jest的mock,咱们可使用一些Jest提供的指定断言(好比toHaveBeenCalledTines),显然更方便。不幸的是,Jest并无提供相似nthCalledWidth(好像快要支持了)这样的API,不然咱们就能够避免这些forEach语句了。但即便这样,一切看起来尚好。

另一件我不喜欢的事是要手动保存originalGetWinner,而后在测试结束后恢复原状。还要那些烦人的eslint注释(这很重要,咱们一下子会专门说这个)。接下来,咱们看一下咱们能不能用Jest提供的工具把咱们的代码进一步简化。

第4步

幸运的是,Jest有一个工具函数叫spyOn,提供了咱们所需的功能。

import thumbWar from '../thumb-war'
import * as utils from '../utils'
test('returns winner', () => {
  jest.spyOn(utils, 'getWinner')
  utils.getWinner.mockImplementation((p1, p2) => p2)
  const winner = thumbWar('Ken Wheeler', 'Kent C. Dodds')
  expect(winner).toBe('Kent C. Dodds')
  utils.getWinner.mockRestore()
})
复制代码

不错,代码确实简单了很多。Mock函数又被叫作spy(这也是为啥这个API叫spyOn)。默认Jest会保存getWinner的原始实现,而且追踪它是如何被调用的。咱们不但愿原始的实现被调用,因此咱们用mockImplementation去指定咱们调用它时应该返回什么结果。最后,咱们再用mockRestore去清除mock操做,以保留getWinner原本的与昂子。(跟咱们以前所作的同样,对吧)。

还记得以前咱们提到的eslint error吗,咱们接下来解决这个问题。

第5步

咱们遇到的ESLint报错很是重要。咱们之因此会遇到这个问题,是由于咱们写代码的方式致使eslint-plugin-import不能静态检测咱们是否破坏了它的规则。这个规则很是重要,就是:import/namespace。之因此咱们会破坏这个规则是由于对import命名空间的成员进行了赋值

为啥这会是个问题呢?由于咱们的ES6代码被Babel转成了CommonJS的形式,而CommonJS中有所谓的require缓存。当我import 一个模块时,我其实是在import哪一个模块中函数的执行环境。因此当我在不一样的文件引入相同的模块,并尝试去修改这个执行环境,这个修改仅对当前文件有效。因此若是你很依赖这个特性,你极可能在升级ES6模块时遇到坑。

Jest模拟了一套模块系统,从而能够很是容易的无缝将咱们的mock实现替换掉原始实现,如今咱们的测试变成了这个样子:

import thumbWar from '../thumb-war'
import * as utilsMock from '../utils'
jest.mock('../utils', () => {
  return {
    getWinner: jest.fn((p1, p2) => p2),
  }
})
test('returns winner', () => {
  const winner = thumbWar('Ken Wheeler', 'Kent C. Dodds')
  expect(winner).toBe('Kent C. Dodds')
  expect(utilsMock.getWinner).toHaveBeenCalledTimes(2)
  utilsMock.getWinner.mock.calls.forEach(args => {
    expect(args).toEqual(['Ken Wheeler', 'Kent C. Dodds'])
  })
})
复制代码

咱们直接告诉Jest咱们但愿全部的文件去使用咱们的mock版本。注意我修改了import过来的名字为utilsMock。这不是必须的,可是我喜欢用这种方式代表这里import过来的是个mock版本而非原始实现。

常见问题:若是你想要仅mock某个模块中的一个函数,也许你想看看require.requireActualAPI

第6步

到这里就几乎快要说完了。假如咱们要在多个测试中用到getWinner函数,可是又不想处处复制粘贴这段mock代码怎么办?这就须要用到__mocks__文件夹提供方便了。因此咱们在咱们想要对其mock的文件旁边建立一个__mocks__文件夹,而后建立一个相同名字的文件:

other/whats-a-mock/
├── __mocks__
│   └── utils.js
├── __tests__/
├── thumb-war.js
└── utils.js
复制代码

__mocks__/utils.js文件中,咱们这么写:

// __mocks__/utils.js
export const getWinner = jest.fn((p1, p2) => p2)
复制代码

这样咱们的测试能够写成:

// __tests__/thumb-war.js
import thumbWar from '../thumb-war'
import * as utilsMock from '../utils'
jest.mock('../utils')
test('returns winner', () => {
  const winner = thumbWar('Ken Wheeler', 'Kent C. Dodds')
  expect(winner).toBe('Kent C. Dodds')
  expect(utilsMock.getWinner).toHaveBeenCalledTimes(2)
  utilsMock.getWinner.mock.calls.forEach(args => {
    expect(args).toEqual(['Ken Wheeler', 'Kent C. Dodds'])
  })
})
复制代码

如今咱们只须要写jest.mock(pathToModule)就能够了,它会自动使用咱们刚才建立的mock实现。

咱们也许不想mock实现老是返回第二个选手获胜,这时咱们就能够针对特定的测试用mockImplementation给出指望的实现,进而测试其余状况是否测试经过。你也能够在你的mock中使用一些工具库方法,想怎么玩儿都行。

End.

相关文章
相关标签/搜索