只模拟一个模块中的函数,但保留其余函数的原始功能

196

我只想嘲笑一个模块中的单个函数(命名导出),但保留模块其余函数不变。

使用jest.mock('package-name')会使所有导出的函数都被模拟,而这不是我想要的。

我尝试将命名导出展开到模拟对象中...

import * as utils from './utilities.js';

jest.mock(utils, () => ({
  ...utils
  speak: jest.fn(),
}));

但是却遇到了这个错误:

jest.mock() 的模块工厂不允许引用任何超出作用域的变量。

Translated text:

But encountered this error:

The module factory of jest.mock() is not allowed to reference any out-of-scope variables.


对于那些跟随者来说,jest.mock()实际上像变量一样得到提升。因此,它们在导入之前被调用。 - Tony
6个回答

301

这篇答案的亮点是 jest.requireActual(),这是一个非常有用的工具,它告诉jest“嘿,保留每个原始功能并将其导入”。

jest.mock('./utilities.js', () => ({
  ...jest.requireActual('./utilities.js'),
  speak: jest.fn(),
}));

让我们来看另一个常见的场景,你正在使用enzyme ShallowWrapper,但它与useContext() hook不兼容,那么你该怎么办呢?虽然我确定有多种方法,但这是我喜欢的方法:

import React from "react";

jest.mock("react", () => ({
  ...jest.requireActual("react"), // import and retain the original functionalities
  useContext: jest.fn().mockReturnValue({foo: 'bar'}) // overwrite useContext
}))

这样做的好处是您仍然可以在原始代码中使用import React, { useContext } from "react",而无需担心将它们转换为React.useContext(),就像您使用jest.spyOn(React, 'useContext')时一样。


7
Jest 27 不再支持这个方法:Spread types may only be created from object types. (这个方法已失效) - ypicard
7
您需要先将jest.requireActual('./myModule')存储在一个变量中,然后才能在该变量上使用展开运算符。https://jestjs.io/docs/jest-object#jestrequireactualmodulename - halshing
4
当我使用一个索引文件来存储许多没有默认导出的组件时,这对我不起作用。我收到了“TypeError: Cannot read property 'default' of undefined”错误消息。 - Ben Gooding
21
对我来说它无效。jest.mockjest.requireActual有效,但是当我尝试模拟其中一个函数时,我的代码仍然调用原始实现。 - Kuba K
2
@Popsicle 我也是一样,我的代码一直在调用原始变量,而不是模拟的变量。 - User7723337
显示剩余11条评论

67

最直接的方法是使用jest.spyOn,然后使用.mockImplementation()。这将使模块中的所有其他函数继续按照其定义方式工作。

对于包:

import axios from 'axios';

jest.spyOn(axios, 'get');
axios.get.mockImplementation(() => { /* do thing */ });

对于具有命名导出的模块:

import * as utils from './utilities.js';

jest.spyOn(utils, 'speak');
utils.speak.mockImplementation(() => { /* do thing */ });

文档在这里:https://jestjs.io/docs/en/jest-object#jestspyonobject-methodname


36
如果在我的测试文件中调用该函数,它可以正常工作。但如果该函数在另一个文件中被调用/导入,则无法正常工作。有什么想法? - tanner burton
3
我认为这种解决方法比要求使用扩展语法更加优雅。此外,你可以在 spyOn 调用本身中分配被监视的函数,例如:const speakSpy = jest.spyOn(utils, "speak"); 然后稍后再调用它: speakSpy.mockImplementation(() => { /* stuff */ }); - Emzaw
1
@tannerburton 当与jest.mock()结合使用时,它适用于在其他文件中导入的函数,示例请参见此处:https://medium.com/trabe/mocking-different-values-for-the-same-module-using-jest-a7b8d358d78b - DarthVanger

14

jest.mock 中使用 jest.requireActual 似乎是可行的方法,然而我需要添加一个代理而不是对象展开来防止类型错误 Cannot read properties of undefined (reading ...) 在某些导入场景中可能会发生。

这是最终结果:

jest.mock('the-module-to-mock', () => {
  const actualModule = jest.requireActual('the-module-to-mock')

  return new Proxy(actualModule, {
    get: (target, property) => {
      switch (property) {
        // add cases for exports you want to mock
        // 
        case 'foo': {
          return jest.fn() // add `mockImplementation` etc
        }
        case 'bar': {
          return jest.fn()
        }
        // fallback to the original module
        default: {
          return target[property]
        }
      }
    },
  })
})

这是当文件创建箭头函数时。它们被视为对象属性而不是类函数,所以不会被嘲笑。 - AncientSwordRage
你真是个救命恩人。经历了20个小时的调试和绝望,最终它终于成功了。 - David Fischer
这个不再有效 2023 - user1034912
这是一个黄金解决方案 ⭐️ - undefined

8

对我来说,这个方法管用:

const utils = require('./utilities.js');
...
jest.spyOn(utils, 'speak').mockImplementation(() => jest.fn());

8
如果测试套件不直接调用speak(),那么这个说法是错误的!如果测试调用一个函数,该函数本身调用speak(),那么这种安排将失败! - ankush981

6
我采用了Rico Kahler的答案,并创建了这个通用函数:

我采用了Rico Kahler的答案,并创建了这个通用函数:

function mockPartially(packageName: string, getMocks: (actualModule: any) => any) {
  jest.doMock(packageName, () => {
    const actualModule = jest.requireActual(packageName);
    const mocks = getMocks(actualModule);

    return new Proxy(actualModule, {
      get: (target, property) => {
        if (property in mocks) {
          return mocks[property];
        } else {
          return target[property];
        }
      },
    });
  });
}

你可以这样使用它来模拟lodash:

mockPartially('lodash', (_actualLodash) => { //sometimes you need the actual module
   return {
      'isObject': () => true, //mock isObject
      'isArray': () => true // mock isArray
   }
});

这对我在使用es6模块时没有起作用。Rico的答案有效。 - damdafayton

-5

手动模拟

您可以在与utilities.js同级别的目录中创建__mocks__目录,然后在该目录中创建一个名为utilities.js的文件。

utilities.js
const speak = () => "Function speak";
const add = (x, y) => x + y;
const sub = (x, y) => x - y;

module.exports = { speak, add, sub };

现在,保持一切不变,只需模拟 speak 函数。

__mocks__/utilities.js
const speak = jest.fn(() => "Mocked function speak");
const add = (x, y) => x + y;
const sub = (x, y) => x - y;

module.exports = { speak, add, sub };

现在你可以模拟 utilities.js

utilities.test.js
const { speak, add, sub } = require("./utilities");

jest.mock("./utilities");

test("speak should be mocked", () => {
  expect(speak()).toBe("Mocked function speak");
});

模拟 Node 模块

在与 node_modules 相同的级别上创建一个名为__mocks__的目录,并在此目录中添加文件'axios.js'。

__mocks__/axios.js
const axios = {
  get: () => Promise.resolve({ data: { name: "Mocked name" } }),
};

module.exports = axios;
fetch.js
const axios = require("axios");

const fetch = async () => {
  const { data } = await axios.get(
    "https://jsonplaceholder.typicode.com/users/1"
  );
  return data.name;
};

module.exports = fetch;

使用Node模块,您无需显式调用jest.mock("axios")

fetch.test.js
const fetch = require("./fetch");

test("axios should be mocked", async () => {
  expect(await fetch()).toBe("Mocked name");
});

3
建议人们将代码复制粘贴到模拟文件中并替换所需内容是不现实的。简单来说 - 这是无法扩展的。真实文件的更改不会自动反映在模拟文件中,这意味着这会引入大量脆弱和手动工作。 - Giora Guttsait
你不需要这样做。只需重新导出所有未更改的内容。export thingToMock = jest.fn(); export { fn1, fn2, fn3 } from "../original"; PS:保持你的模块小而专注于一件事情。 - Jarrett Meyer

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接