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

By Ken C. Dodds

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

第0步

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

import {getWinner} from './utils'
function thumbWar(player1, player2) {const numberToWin = 2let player1Wins = 0let player2Wins = 0while (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/namespaceutils.getWinner = (p1, p2) => p2const winner = thumbWar('Ken Wheeler', 'Kent C. Dodds')expect(winner).toBe('Kent C. Dodds')// eslint-disable-next-line import/namespaceutils.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/namespaceutils.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/namespaceutils.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/namespaceutils.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/namespaceutils.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日
原文作者:妖僧风月
本文来源:  掘金  如需转载请联系原作者

到底啥是JavaScript Mock相关推荐

  1. ASP.NET MVC 單元測試系列 (3):瞭解 Mock 假物件 ( moq )

    http://blog.miniasp.com/post/2010/09/16/ASPNET-MVC-Unit-Testing-Part-03-Using-Mock-moq.aspx 我們在上一篇已經 ...

  2. 什么是 JavaScript?

    每篇文章的浪漫主义 凌晨的海或凌晨的山顶,总要去一次吧,趁热夏. 到底什么是JavaScript 广义的定义 它到底可以做什么? JavaScript 在页面上做了什么? 怎样向页面添加 JavaSc ...

  3. JQuery整体简化学习

    一.  简介: 1 关于jQuery jQuery是一个轻量的js库,提供了dom选择.操作.兼容完善的事件机制和Ajax的封装,使之有更为简便和简捷的去开发js程序.JQuery的底层是JavaSc ...

  4. Jest + React Testing Library 单测总结

    大家好,我是若川.持续组织了6个月源码共读活动,感兴趣的可以点此加我微信 ruochuan02 参与,每周大家一起学习200行左右的源码,共同进步.同时极力推荐订阅我写的<学习源码整体架构系列& ...

  5. react jest测试_如何使用Jest和react-testing-library测试Socket.io-client应用程序

    react jest测试 by Justice Mba 由Mba法官 如何使用Jest和react-testing-library测试Socket.io-client应用程序 (How to test ...

  6. 单文件组件下的vue,可以擦出怎样的火花

    2016注定不是个平凡年,无论是中秋节问世的angular2,还是全面走向稳定的React,都免不了面对另一个竞争对手vue2.喜欢vue在设计思路上的"先进性"(原谅我用了这么一 ...

  7. JS字符串string

    关于js 的字符串(string)的知识 javascript的基本数据类型有五种:number   string  boolean  null  undefined . 1.字符串是到底是什么 Ja ...

  8. node.js是用来做什么的?

    Node.js是一个基于Chrome V8引擎的JavaScript运行环境.Node对一些特殊用例进行优化,提供替代的API,使得V8在非浏览器环境下运行得更好. 我们都知道计算机处理器智能识别机器 ...

  9. Javascript中的循环变量声明,到底应该放在哪儿?

    不放走任何一个细节.相信很多Javascript开发者都在声明循环变量时犹 豫过var i到底应该放在哪里:放在不同的位置会对程序的运行产生怎样的影响?哪一种方式符合Javascript的语言规范?哪 ...

最新文章

  1. java怎么定义字符长度_java – 当字符串长度超过列长度定义时,如何以静默方式截断字符串?...
  2. module ‘open3d‘ has no attribute ‘PointCloud‘
  3. H264—MP4格式及在MP4文件中提取H264的SPS、PPS及码流
  4. java echarts 散点图,echarts在地图上绘制散点图(任意点)
  5. Mozilla 发布新 Firefox 用户信息反跟踪策略
  6. 着迷英语900句_开明的系统管理员如何让我着迷于Linux
  7. java 创建类带泛型_java-创建泛型类列表
  8. hibernate框架搭建与使用
  9. 面试中常见智力题汇总
  10. [笔记]numpy中的tile与kron的用法
  11. PS2接口键盘、鼠标改成USB接口
  12. 等比求和模版,下标从1开始
  13. Andorid 拍照、从相册中选择图片兼容7.0uri
  14. Pytorch3d中的倒角损失函数Chamfer Distance Loss的用法(pytorch3d.loss.chamfer_distance)
  15. iterator 的遍历 循环输出数字,页码
  16. 使用 HTML 5 Canvas 和 Raycasting 创建伪 3D 游戏
  17. R语言ggplot2 | 循环画图及导出
  18. 无线路由器WDS桥接设置指南
  19. 使用scaffold-eth脚手架快速构建 Web3 Dapp 应用
  20. P5724 【深基4.习5】求极差 / 最大跨度值

热门文章

  1. 麒麟合盛(APUS)李涛:“入侵”全球市场的底层逻辑
  2. pytorch上配置使用双显卡或多显卡
  3. 最全计算机二级考试攻略(大学生必看)
  4. OPT小讲堂 ∣ SciSmart图像识别之条形码识别、二维码识别
  5. 设计文件加密服务器,如何设计数据库文件加密系统
  6. 工程造价步骤_工程结算六大步骤及体会
  7. TRACE32——变量显示选项Setup.Var
  8. 计算机类——扩展学习计划推荐方向
  9. matlab画波传播,科学网—MATLAB绘制波包曲线 - 李金磊的博文
  10. 微信小程序获取当前城市地址