【问题标题】:Mock only one function from module but leave rest with original functionality仅模拟模块中的一个功能,但保留原始功能
【发布时间】:2020-04-06 07:43:50
【问题描述】:

我只想从一个模块中模拟一个函数(名为导出),但保留其余模块函数不变。

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

我尝试将命名导出传播回模拟对象...

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

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

但出现此错误:

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

【问题讨论】:

  • 对于后面的那些,jest.mock() 实际上得到了hoisted 之类的变量。因此,它们在导入之前被调用。

标签: javascript unit-testing jestjs


【解决方案1】:

最直接的方法是使用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

【讨论】:

  • 如果在我的测试文件中调用该函数,则此方法有效。但是,如果在另一个文件中调用/导入该函数,它就不起作用。有什么想法吗?
  • 我认为这个解决方案比需要扩展语法更优雅。此外,您可以在 spyOn 调用本身期间分配间谍功能,例如:const speakSpy = jest.spyOn(utils, "speak"); 并稍后调用它:speakSpy.mockImplementation(() => { /* stuff */ });
  • @tannerburton 它适用于在其他文件中导入的函数,当与jest.mock()结合使用时,请参见此处的示例:medium.com/trabe/…
【解决方案2】:

对我来说这很有效:

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

【讨论】:

    【解决方案3】:

    这个答案的亮点是 jest.requireActual(),这是一个非常有用的实用程序,它开玩笑说“嘿,保持所有原始功能完整并导入它们”。

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

    让我们来看看另一个常见的场景,你正在使用酶 ShallowWrapper,它与 useContext() 挂钩并不顺利,那么你会怎么做?虽然我确定有多种方法,但这是我喜欢的一种:

    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')

    【讨论】:

    • 这对我有用。 spyOn 在我的情况下没用,因为我的测试文件中没有调用该方法。
    • 谢谢!这解决了我在尝试从 node_modules 中的模块模拟​​单个导出函数时遇到的问题
    • 这在 Jest 27 中不再起作用:Spread types may only be created from object types
    • @ypicard 你必须先将jest.requireActual('./myModule') 存储在一个变量中,然后你可以在变量上使用扩展运算符。 jestjs.io/docs/jest-object#jestrequireactualmodulename
    • 当我使用索引文件存储许多没有默认导出的组件时,这对我不起作用。我得到TypeError: Cannot read property 'default' of undefined
    【解决方案4】:

    手动模拟

    您可以在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_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;
    

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

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

    【讨论】:

    • 建议人们将他们的代码复制粘贴到模拟文件中并替换他们需要的任何内容是不现实的。简而言之 - 它无法扩展。对真实文件的更改不会自动反映在模拟文件中,这意味着这会引入很多脆弱和手动的工作。
    • 你不需要。只需重新导出所有未更改的内容。 export thingToMock = jest.fn(); export { fn1, fn2, fn3 } from "../original"; PS:保持你的模块小并专注于一件事。
    【解决方案5】:

    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]
            }
          }
        },
      })
    })
    

    【讨论】:

    • 这是文件创建箭头函数的时候。我认为它们是在对象属性而不是类函数中处理的,所以不要以同样的方式被嘲笑。
    猜你喜欢
    • 2020-07-13
    • 2019-07-16
    • 1970-01-01
    • 1970-01-01
    • 2015-05-21
    • 2019-08-28
    • 2020-12-06
    • 2014-08-29
    • 1970-01-01
    相关资源
    最近更新 更多