[Testing] JavaScript Mocking Fundamentals

本文详细介绍如何使用JavaScript的mock函数进行单元测试,包括如何确保函数被正确调用,使用jest.fn和jest.spyOn创建mock函数,以及如何mock整个模块和创建共享mock模块。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

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

 

转载于:https://www.cnblogs.com/Answer1215/p/9905082.html

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值