开发者社区> 我是小助手> 正文

到底啥是JavaScript Mock

简介:
+关注继续查看

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

By Ken C. Dodds

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

第0步

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


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才能可靠地测试你的代码的情景。(这里为了简化,假设这个函数是同步的)

另外,除了重新实现一遍getWinner的逻辑,我们实际上不太可能做出有用的判断以确定猜拳游戏中到底是谁获胜了。所以,没有mocking的情况下,下面就是我们能给出的最好的测试了:

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


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)的形式。下面给出一个例子:

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


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
})

看上面的代码,你可以注意到以下几点:1、我们必须采用import * as的形式引入utils,以便于接下来可以操作这个对象(后面会谈到,这种形式有啥坏处)。2、我们需要先把要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.



原文发布时间为:2018年06月24日
原文作者:妖僧风月
本文来源: 掘金 如需转载请联系原作者


版权声明:本文内容由阿里云实名注册用户自发贡献,版权归原作者所有,阿里云开发者社区不拥有其著作权,亦不承担相应法律责任。具体规则请查看《阿里云开发者社区用户服务协议》和《阿里云开发者社区知识产权保护指引》。如果您发现本社区中有涉嫌抄袭的内容,填写侵权投诉表单进行举报,一经查实,本社区将立刻删除涉嫌侵权内容。

相关文章
阿里云服务器如何登录?阿里云服务器的三种登录方法
购买阿里云ECS云服务器后如何登录?场景不同,大概有三种登录方式:
9819 0
使用SSH远程登录阿里云ECS服务器
远程连接服务器以及配置环境
13673 0
阿里云服务器怎么设置密码?怎么停机?怎么重启服务器?
如果在创建实例时没有设置密码,或者密码丢失,您可以在控制台上重新设置实例的登录密码。本文仅描述如何在 ECS 管理控制台上修改实例登录密码。
20519 0
阿里云服务器如何登录?阿里云服务器的三种登录方法
购买阿里云ECS云服务器后如何登录?场景不同,云吞铺子总结大概有三种登录方式: 登录到ECS云服务器控制台 在ECS云服务器控制台用户可以更改密码、更换系统盘、创建快照、配置安全组等操作如何登录ECS云服务器控制台? 1、先登录到阿里云ECS服务器控制台 2、点击顶部的“控制台” 3、通过左侧栏,切换到“云服务器ECS”即可,如下图所示 通过ECS控制台的远程连接来登录到云服务器 阿里云ECS云服务器自带远程连接功能,使用该功能可以登录到云服务器,简单且方便,如下图:点击“远程连接”,第一次连接会自动生成6位数字密码,输入密码即可登录到云服务器上。
33439 0
阿里云服务器端口号设置
阿里云服务器初级使用者可能面临的问题之一. 使用tomcat或者其他服务器软件设置端口号后,比如 一些不是默认的, mysql的 3306, mssql的1433,有时候打不开网页, 原因是没有在ecs安全组去设置这个端口号. 解决: 点击ecs下网络和安全下的安全组 在弹出的安全组中,如果没有就新建安全组,然后点击配置规则 最后如上图点击添加...或快速创建.   have fun!  将编程看作是一门艺术,而不单单是个技术。
18795 0
使用NAT网关轻松为单台云服务器设置多个公网IP
在应用中,有时会遇到用户询问如何使单台云服务器具备多个公网IP的问题。 具体如何操作呢,有了NAT网关这个也不是难题。
35189 0
阿里云服务器如何登录?阿里云服务器的三种登录方法
购买阿里云ECS云服务器后如何登录?场景不同,阿里云优惠总结大概有三种登录方式: 登录到ECS云服务器控制台 在ECS云服务器控制台用户可以更改密码、更换系.
25140 0
windows server 2008阿里云ECS服务器安全设置
最近我们Sinesafe安全公司在为客户使用阿里云ecs服务器做安全的过程中,发现服务器基础安全性都没有做。为了为站长们提供更加有效的安全基础解决方案,我们Sinesafe将对阿里云服务器win2008 系统进行基础安全部署实战过程! 比较重要的几部分 1.
11849 0
+关注
我是小助手
云栖直播
384
文章
6
问答
文章排行榜
最热
最新
相关电子书
更多
JS零基础入门教程(上册)
立即下载
性能优化方法论
立即下载
手把手学习日志服务SLS,云启实验室实战指南
立即下载