Ensure Functions are Called Correctly with JavaScript Mocks

Often when writing JavaScript tests and mocking dependencies, you’ll want to verify that the function was called correctly. That requires keeping track of how often the function was called and what arguments it was called with. That way we can make assertions on how many times it was called and ensure it was called with the right arguments.

Function to be mocked: utils.js

// returns the winning player or null for a tie
// Let's pretend this isn't using Math.random() but instead
// is making a call to some third party machine learning
// service that has a testing environment we don't control
// and is unreliable so we want to mock it out for tests.
function getWinner(player1, player2) {
const winningNumber = Math.random();
return winningNumber < 1 / 3
? player1
: winningNumber < 2 / 3
? player2
: null;
} module.exports = {getWinner};

Implementaion: thumbwar.js

const utils = require("./utils");

function thumbWar(player1, player2) {
const numberToWin = 2;
let player1Wins = 0;
let player2Wins = 0;
while (player1Wins < numberToWin && player2Wins < numberToWin) {
const winner = utils.getWinner(player1, player2);
if (winner === player1) {
player1Wins++;
} else if (winner === player2) {
player2Wins++;
}
}
return player1Wins > player2Wins ? player1 : player2;
} module.exports = thumbWar;

Testing:

const thumbWar = require("./thumbwar");
const utils = require("./utils");
const assert = require("assert"); test("returns winner", () => {
const originalGetWinner = utils.getWinner;
utils.getWinner = jest.fn((p1, p2) => p1); // eslint-disable-line no-unused-vars
const winner = thumbWar("KCD", "KW");
expect(winner).toBe("KCD");
// check the params are correct
expect(utils.getWinner.mock.calls).toEqual([["KCD", "KW"], ["KCD", "KW"]]);
// check the fn has been called number of times
expect(utils.getWinner).toHaveBeenCalledTimes(2);
// check each time call the fn with the correct params
expect(utils.getWinner).toHaveBeenNthCalledWith(1, "KCD", "KW");
expect(utils.getWinner).toHaveBeenNthCalledWith(2, "KCD", "KW"); utils.getWinner = originalGetWinner;
});

Here we are using 'jest.fn' to mock the function.

We can also create a mock fn by ourselves.

function fn(impl) {
const mockFn = (...args) => {
mockFn.mock.calls.push(args);
return impl(...args);
};
mockFn.mock = {calls: []};
return mockFn;
}
test("returns winner: fn", () => {
const originalGetWinner = utils.getWinner;
utils.getWinner = fn((p1, p2) => p1); // eslint-disable-line no-unused-vars
const winner = thumbWar("KCD", "KW");
assert.strictEqual(winner, "KCD");
assert.deepStrictEqual(utils.getWinner.mock.calls, [
["KCD", "KW"],
["KCD", "KW"],
]);
utils.getWinner = originalGetWinner;
});

Restore the Original Implementation of a Mocked JavaScript Function with jest.spyOn

With our current usage of the mock function we have to manually keep track of the original implementation so we can cleanup after ourselves to keep our tests idempotent (moonkey patching). Let’s see how jest.spyOn can help us avoid the bookkeeping and simplify our situation.

test("returns winner", () => {
//const originalGetWinner = utils.getWinner;
//utils.getWinner = jest.fn((p1, p2) => p1); // eslint-disable-line no-unused-vars
jest.spyOn(utils, "getWinner");
utils.getWinner.mockImplementation((p1, p2) => p1); // eslint-disable-line no-unused-vars
const winner = thumbWar("KCD", "KW");
expect(winner).toBe("KCD");
expect(utils.getWinner.mock.calls).toEqual([["KCD", "KW"], ["KCD", "KW"]]);
expect(utils.getWinner).toHaveBeenCalledTimes(2);
expect(utils.getWinner).toHaveBeenNthCalledWith(1, "KCD", "KW");
expect(utils.getWinner).toHaveBeenNthCalledWith(2, "KCD", "KW"); // utils.getWinner = originalGetWinner;
utils.getWinner.mockRestore();
});

Here we are using jest.spyOn function.

We can also write spyOn function by ourselves.

function fn(impl = () => {}) {
const mockFn = (...args) => {
mockFn.mock.calls.push(args);
mockFn.mockImplementation = newImpl => (impl = newImpl);
return impl(...args);
};
mockFn.mock = {calls: []};
return mockFn;
} function spyOn(obj, prop) {
// store the origianl fn
const originalValue = obj[prop];
// assign new mock fn
obj[prop] = fn;
// add restore fn
obj[prop].mockRestore = () => (obj[prop] = originalValue);
} test("returns winner: fn", () => {
spyOn(utils, "getWinner");
utils.getWinner.mockImplementation = fn((p1, p2) => p1); // eslint-disable-line no-unused-vars
const winner = thumbWar("KCD", "KW");
assert.strictEqual(winner, "KCD");
assert.deepStrictEqual(utils.getWinner.mock.calls, [
["KCD", "KW"],
["KCD", "KW"],
]);
utils.getWinner.mockRestore();
});

Mock a JavaScript module in a test

So far we’re still basically monkey-patching the utils module which is fine, but could lead to problems in the future, especially if we want to mock a ESModule export which doesn’t allow this kind of monkey-patching on exports. Instead, let’s mock the entire module so when our test subject requires the file they get our mocked version instead.

To mock a whole module. we can use 'jest.mock':

const thumbWar = require("./thumbwar");
const utils = require("./utils");
const assert = require("assert"); jest.mock("./utils", () => {
return {
getWinner: jest.fn((p1, p2) => p1), // eslint-disable-line no-unused-vars
};
}); test("returns winner", () => { const winner = thumbWar("KCD", "KW");
expect(winner).toBe("KCD");
expect(utils.getWinner.mock.calls).toEqual([["KCD", "KW"], ["KCD", "KW"]]);
expect(utils.getWinner).toHaveBeenCalledTimes(2);
expect(utils.getWinner).toHaveBeenNthCalledWith(1, "KCD", "KW");
expect(utils.getWinner).toHaveBeenNthCalledWith(2, "KCD", "KW"); utils.getWinner.mockReset();
});

Now we don't need to mock the 'getWinner' function inside test, 'jest.mock' can be used anywhere, jest will make sure it mock will be hoisted to the top.

Make a shared JavaScript mock module

Often you’ll want to mock the same file throughout all the tests in your codebase. So let’s make a shared mock file in Jest's __mocks__ directory which Jest can load for us automatically.

__mocks__/utils.js:

module.exports = {
getWinner: jest.fn((p1, p2) => p1), // eslint-disable-line no-unused-vars
};
const thumbWar = require("../thumbwar");
const utils = require("../utils");
const assert = require("assert"); jest.mock("../utils"); test("returns winner", () => {
const winner = thumbWar("KCD", "KW");
expect(winner).toBe("KCD");
expect(utils.getWinner.mock.calls).toEqual([["KCD", "KW"], ["KCD", "KW"]]);
expect(utils.getWinner).toHaveBeenCalledTimes(2);
expect(utils.getWinner).toHaveBeenNthCalledWith(1, "KCD", "KW");
expect(utils.getWinner).toHaveBeenNthCalledWith(2, "KCD", "KW"); utils.getWinner.mockReset();
});

最新文章

  1. 30秒搞定javascript作用域
  2. Daily Scrum02 12.11
  3. BZOJ4563: [Haoi2016]放棋子
  4. 二十二、Java基础--------GUI入门
  5. jquery工具类函数
  6. Backbone.js
  7. wifi强度数据采集器(android)
  8. translateZ 带来的Z-index 问题
  9. jquery 根据年 月设置报表表头
  10. Zookeeper相关知识
  11. 【转】第7篇:Xilium CefGlue 关于 CLR Object 与 JS 交互类库封装报告:全自动注册与反射方法分析
  12. 用SSH连接SSH连接nitrous.io
  13. 惠普 Compaq 6520s 无线开关打不开
  14. tp框架之对列表的一系列操作及跳转页面(详细步骤)
  15. duilib基本流程
  16. hdu 5919 主席树(区间不同数的个数 + 区间第k大)
  17. mysql ssh 跳板机(堡垒机???)连接服务器
  18. 【转】jenkins+gitlab配置遇到问题
  19. Fragment传参
  20. Ajax+Struts2用户注册功能实现

热门文章

  1. Shell脚本的条件测试与比较
  2. perl学习之:正则表达式
  3. Elasticsearchs的安装/laravel-scout和laravel-scout-elastic的安装
  4. 前端,基础选择器,嵌套关系.display属性,盒模型
  5. 基于链式链表的栈链式存储的C风格实现
  6. Python中正则表达式讲解
  7. 【10】css hack原理及常用hack
  8. 在xcode上把你的app多语言国际化(NSLocalizedString)
  9. 【PL/SQL编程基础】
  10. PHP过滤器 filter_has_var() 函数