[Testing] JavaScript Mocking Fundamentals

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};
View Code
复制代码

 

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;
View Code
复制代码

 

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(); });
复制代码

 

posted @   Zhentiw  阅读(429)  评论(0编辑  收藏  举报
编辑推荐:
· SQL Server 2025 AI相关能力初探
· Linux系列:如何用 C#调用 C方法造成内存泄露
· AI与.NET技术实操系列(二):开始使用ML.NET
· 记一次.NET内存居高不下排查解决与启示
· 探究高空视频全景AR技术的实现原理
阅读排行:
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· AI技术革命,工作效率10个最佳AI工具
历史上的今天:
2017-11-06 [React Native] Animate the Scale of a React Native Button using Animated.spring
2017-11-06 [React] Use the URL as the source of truth in React
2015-11-06 [Angular 2] Validation
2015-11-06 [Angualr 2] Using FormBuilder
2015-11-06 [Angular 2] ng-control & ng-control-group
2015-11-06 [Angular 2] NgNonBindable
点击右上角即可分享
微信分享提示