ChatGPT解决这个技术问题 Extra ChatGPT

未模拟模块时如何在 Jest 中模拟导入的命名函数

我有以下模块正在尝试在 Jest 中进行测试:

// myModule.js

export function otherFn() {
  console.log('do something');
}

export function testFn() {
  otherFn();

  // do other things
}

如上所示,它导出了一些命名函数,重要的是 testFn 使用了 otherFn

在 Jest 中,当我为 testFn 编写单元测试时,我想模拟 otherFn 函数,因为我不希望 otherFn 中的错误影响我对 testFn 的单元测试。我的问题是我不确定最好的方法:

// myModule.test.js
jest.unmock('myModule');

import { testFn, otherFn } from 'myModule';

describe('test category', () => {
  it('tests something about testFn', () => {
    // I want to mock "otherFn" here but can't reassign
    // a.k.a. can't do otherFn = jest.fn()
  });
});

任何帮助/见解表示赞赏。

我不会这样做的。无论如何,嘲笑通常不是您想做的事情。如果您需要模拟某些东西(由于进行服务器调用/等),那么您应该将 otherFn 提取到一个单独的模块中并模拟它。
@kentcdodds,您能否澄清一下“嘲笑通常不是您想做的事情”的意思?这似乎是一个相当宽泛(过于宽泛?)的陈述,因为嘲笑肯定是经常使用的东西,大概是出于(至少某些)充分的理由。那么,您是否可能指的是为什么在这里嘲笑可能不好,或者您的意思是一般意义上的?
模拟通常是测试实现细节。尤其是在这个级别上,它导致的测试并没有真正验证您的测试是否有效(而不是您的代码有效)这一事实。
在我意识到它是谁之前,我正准备称第一个评论者为白痴。不过,这条评论让我很困惑。首先,模拟对于测试现代应用程序至关重要。另外,我也有同样的情况,将功能分成单独的模块是没有意义的。
@kentcdodds 那么,如果我的组件进行了 api 调用,我怎么能不模拟它来测试渲染的输出呢?我不认为模拟 api 调用是在测试实现细节。例如,如果我的 api 调用在页面上填充用户数据,我的组件的输出是包含数据的页面,所以没有它我无法真正进行测试。正确的?我仍在使用输入(例如 userId 道具)测试输出。我没有测试是否进行了 api 调用。还是我完全误解了?

P
Patrick

在 jest.mock() 中使用 jest.requireActual()

jest.requireActual(moduleName) 返回实际模块而不是模拟,绕过对模块是否应该接收模拟实现的所有检查。

例子

我更喜欢这种简洁的用法,您需要并在返回的对象中传播:

// myModule.test.js

import { otherFn } from './myModule.js'

jest.mock('./myModule.js', () => ({
  ...(jest.requireActual('./myModule.js')),
  otherFn: jest.fn()
}))

describe('test category', () => {
  it('tests something about otherFn', () => {
    otherFn.mockReturnValue('foo')
    expect(otherFn()).toBe('foo')
  })
})

在 Jest 的 Manual Mocks 文档中也引用了此方法(接近 Examples 的末尾):

为了确保手动模拟与其实际实现保持同步,在手动模拟中使用 jest.requireActual(moduleName) 要求真实模块并在导出之前使用模拟函数对其进行修改可能很有用。


Fwiw,您可以通过删除 return 语句并将箭头函数体包装在括号中来使其更加简洁:例如。 jest.mock('./myModule', () => ({ ...jest.requireActual('./myModule'), otherFn: () => {}}))
这很好用!这应该是公认的答案。
不适合我: jest.mock('../../src/helpers', () => ({ ...jest.requireActual('../../src/helpers'), getCurrentMessage: (): { getFrom: () => string } => { return { getFrom: (): string => '测试用户 <test.user@example.com>', }; }, })) ;我正在测试的函数仍在调用实际的 getCurrentMessage
...jest.requireActual 不适用于我,因为我有使用 babel 的路径别名.. 可以与 ...require.requireActual 一起使用,也可以在从路径中删除别名后使用
这个答案实际上并没有回答这个问题。提供的解决方案不调用 testFn,而应调用 otherFn 的模拟。如果您调用 testFn,它仍将使用原始 otherFn。至少,它在 TypeScript 中对我不起作用。请参阅 explaination
B
Brian Adams

看起来我参加这个聚会迟到了,但是是的,这是可能的。

testFn 只需调用 otherFn 使用模块

如果 testFn 使用模块调用 otherFn,则可以模拟 otherFn 的模块导出,testFn 将调用模拟。

这是一个工作示例:

我的模块.js

import * as myModule from './myModule';  // import myModule into itself

export function otherFn() {
  return 'original value';
}

export function testFn() {
  const result = myModule.otherFn();  // call otherFn using the module

  // do other things

  return result;
}

myModule.test.js

import * as myModule from './myModule';

describe('test category', () => {
  it('tests something about testFn', () => {
    const mock = jest.spyOn(myModule, 'otherFn');  // spy on otherFn
    mock.mockReturnValue('mocked value');  // mock the return value

    expect(myModule.testFn()).toBe('mocked value');  // SUCCESS

    mock.mockRestore();  // restore otherFn
  });
});

这本质上是 Facebook 使用的方法的 ES6 版本,由 Facebook 开发人员在 this post 中间进行了描述。
而不是将 myModule 导入自身,只需调用 exports.otherFn()
@andrhamm exports 在 ES6 中不存在。调用 exports.otherFn() 现在可以工作,因为 ES6 正在被编译为更早的模块语法,但是当 ES6 被原生支持时它会中断。
现在有这个确切的问题,我确定我以前遇到过这个问题。我不得不删除大量的导出。<方法名>帮助摇晃树木,它打破了许多测试。我会看看这是否有任何影响,但它似乎很hacky。我已经多次遇到这个问题,就像其他答案所说的那样,像 babel-plugin-rewire 甚至更好的东西,npmjs.com/package/rewiremock 我很确定它也可以做到以上。
是否可以模拟一个抛出而不是模拟一个返回值?编辑:你可以,这里是stackoverflow.com/a/50656680/2548010
b
bobu
import m from '../myModule';

对我不起作用,我确实使用过:

import * as m from '../myModule';

m.otherFn = jest.fn();

测试后如何恢复 otherFn 的原始功能,使其不干扰其他测试?
我认为您可以配置 jest 以在每次测试后清除模拟?来自文档:“clearMocks 配置选项可用于在测试之间自动清除模拟。”。您可以设置 clearkMocks: true jest package.json 配置。 facebook.github.io/jest/docs/en/mock-function-api.html
如果这是更改全局状态的问题,您始终可以将原始功能存储在某种测试变量中并在测试后将其带回
常量原始; beforeAll(() = > { original = m.otherFn; m.otherFn = jest.fn(); }) afterAll(() => { m.otherFn = original; }) 它应该可以工作,但是我没有测试它
J
Jack Kinsey

我知道很久以前有人问过这个问题,但我只是遇到了这种情况,终于找到了一个可行的解决方案。所以我想我会在这里分享。

对于模块:

// myModule.js

export function otherFn() {
  console.log('do something');
}

export function testFn() {
  otherFn();

  // do other things
}

您可以更改为以下内容:

// myModule.js

export const otherFn = () => {
  console.log('do something');
}

export const testFn = () => {
  otherFn();

  // do other things
}

将它们导出为常量而不是函数。我相信这个问题与 JavaScript 中的提升有关,并且使用 const 可以防止这种行为。

然后在您的测试中,您可以有如下内容:

import * as myModule from 'myModule';


describe('...', () => {
  jest.spyOn(myModule, 'otherFn').mockReturnValue('what ever you want to return');

  // or

  myModule.otherFn = jest.fn(() => {
    // your mock implementation
  });
});

您的模拟现在应该可以正常工作了。


我现在遇到了同样的问题,我想知道如果 otherFn 不是一个函数,而是一个数组,你将如何模拟它?所以 testFn 正在读取一个也从模块导出的数组,我想在我的测试中模拟该数组以更改 testFn 的行为而不模拟自身。
v
vutran

转译后的代码将不允许 babel 检索 otherFn() 所指的绑定。如果您使用函数表达式,您应该能够实现模拟otherFn()

// myModule.js
exports.otherFn = () => {
  console.log('do something');
}

exports.testFn = () => {
  exports.otherFn();

  // do other things
}

// myModule.test.js
import m from '../myModule';

m.otherFn = jest.fn();

但正如@kentcdodds 在上一条评论中提到的,您可能不想模拟 otherFn()。相反,只需为 otherFn() 编写一个新规范并模拟它正在进行的任何必要调用。

例如,如果 otherFn() 正在发出一个 http 请求...

// myModule.js
exports.otherFn = () => {
  http.get('http://some-api.com', (res) => {
    // handle stuff
  });
};

在这里,您需要模拟 http.get 并根据模拟的实现更新您的断言。

// myModule.test.js
jest.mock('http', () => ({
  get: jest.fn(() => {
    console.log('test');
  }),
}));

如果其他几个模块使用 otherFn 和 testFn 怎么办?您是否需要在使用这两个模块(无论堆栈有多深)的所有测试文件中设置 http 模拟?另外,如果您已经对 testFn 进行了测试,为什么不在使用 testFn 的模块中直接存根 testFn 而不是 http?
因此,如果 otherFn 被破坏,所有依赖于该测试的测试都将失败。此外,如果 otherFn 内部有 5 个 if,您可能需要测试您的 testFn 是否适用于所有这些子案例。您现在将有更多的代码路径要测试。
e
ezze

基于 Brian Adams' answer,这就是我能够在 TypeScript 中使用相同方法的方式。此外,使用 jest.doMock() 可以仅在测试文件的某些特定测试中模拟模块函数,并为每个模块提供单独的模拟实现。

src/module.ts

import * as module from './module';

function foo(): string {
  return `foo${module.bar()}`;
}

function bar(): string {
  return 'bar';
}

export { foo, bar };

测试/module.test.ts

import { mockModulePartially } from './helpers';

import * as module from '../src/module';

const { foo } = module;

describe('test suite', () => {
  beforeEach(function() {
    jest.resetModules();
  });

  it('do not mock bar 1', async() => {
    expect(foo()).toEqual('foobar');
  });

  it('mock bar', async() => {
    mockModulePartially('../src/module', () => ({
      bar: jest.fn().mockImplementation(() => 'BAR')
    }));
    const module = await import('../src/module');
    const { foo } = module;
    expect(foo()).toEqual('fooBAR');
  });

  it('do not mock bar 2', async() => {
    expect(foo()).toEqual('foobar');
  });
});

测试/helpers.ts

export function mockModulePartially(
  modulePath: string,
  mocksCreator: (originalModule: any) => Record<string, any>
): void {
  const testRelativePath = path.relative(path.dirname(expect.getState().testPath), __dirname);
  const fixedModulePath = path.relative(testRelativePath, modulePath);
  jest.doMock(fixedModulePath, () => {
    const originalModule = jest.requireActual(fixedModulePath);
    return { ...originalModule, ...mocksCreator(originalModule) };
  });
}

模块的模拟函数被移动到位于单独文件中的辅助函数 mockModulePartially,因此可以从不同的测试文件(通常可以位于其他目录中)使用它。它依赖 expect.getState().testPath 来修复正在模拟的模块 (modulePath) 的路径(使其相对于包含 mockModulePartiallyhelpers.ts)。作为第二个参数传递给 mockModulePartiallymocksCreator 函数应返回模块的模拟。此函数接收 originalModule 并且模拟实现可以选择依赖它。


d
demaroar

我通过在这里找到的各种答案解决了我的问题:

我的模块.js

import * as myModule from './myModule';  // import myModule into itself

export function otherFn() {
  return 'original value';
}

export function testFn() {
  const result = myModule.otherFn();  // call otherFn using the module

  // do other things

  return result;
}

myModule.test.js

import * as myModule from './myModule';

describe('test category', () => {
  let otherFnOrig;

  beforeAll(() => {
    otherFnOrig = myModule.otherFn;
    myModule.otherFn = jest.fn();
  });

  afterAll(() => {
    myModule.otherFn = otherFnOrig;
  });

  it('tests something about testFn', () => {
    // using mock to make the tests
  });
});

s
schrodinger's code

除了这里的第一个答案,您还可以使用 babel-plugin-rewire 来模拟导入的命名函数。您可以从表面上查看 named function rewiring 部分。

对于您的情况,其中一个直接好处是您无需更改从函数中调用其他函数的方式。


如何配置 babel-plugin-rewire 以使用 node.js?