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


111

我有以下模块要在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()
  });
});

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


7
我不会的 无论如何,通常都不是要模拟。而且,如果您需要模拟某些东西(由于进行服务器调用等),则应提取otherFn到一个单独的模块中并对其进行模拟。
kentcdodds

2
我还在用@jrubins使用的相同方法进行测试。测试行为function A谁来电function B,但我不想执行真正落实function B,因为我想只是测试中实现的逻辑function A
jplaza

44
@kentcdodds,您能否说明“模拟通常不是您想做的事”的意思?这似乎是一个相当广泛(过于广泛?)的陈述,因为嘲笑无疑是经常使用的东西,大概是出于(至少有一些)很好的理由。那么,您也许是在指为什么在这里嘲笑可能不是一件好事,或者您通常是真的意思吗?
安德鲁·威廉姆斯

2
嘲笑通常是测试实现细节。尤其是在此级别上,它导致的测试实际上并不能证明您的测试有效(而不是代码有效)。
kentcdodds

3
记录下来,自从几年前写了这个问题以来,我改变了对自己想做多少嘲笑的态度(不再做这种嘲笑了)。这些天,我非常同意@kentcdodds和他的测试理念(并强烈推荐他的博客以及@testing-library/react那里的任何Reacters),但是我知道这是一个有争议的主题。
乔恩·鲁宾斯

Answers:


100

jest.requireActual()内部使用jest.mock()

jest.requireActual(moduleName)

返回实际模块而不是模拟模块,绕过所有有关该模块是否应接收模拟实施的检查。

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

// myModule.test.js

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

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

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

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

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


4
首先,您可以通过删除return语句并将箭头函数的主体括在括号中来使其更加简洁:jest.mock('./myModule', () => ({ ...jest.requireActual('./myModule'), otherFn: () => {}}))
Nick F

2
这很棒!这应该是公认的答案。
JR

2
...jest.requireActual不适用于我,因为我使用babel进行了路径别名。....require.requireActual从路径中删除别名后
都可工作

1
otherFun在这种情况下,您将如何测试该调用?假设otherFn: jest.fn()
Stevula

1
@Stevula我更新了答案以显示一个真实的用法示例。我展示了mockReturnValue一种方法来更好地证明模拟版本正在被调用,而不是原始版本,但是如果您真的只想查看它是否已被调用而未声明返回值,则可以使用jest匹配器.toHaveBeenCalled()
gfullam

36
import m from '../myModule';

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

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

m.otherFn = jest.fn();

5
测试后如何恢复otherFn的原始功能,以免干扰其他testS?
Aequitas

1
我认为您可以在每次测试后配置笑话来清除模拟吗?从文档中:“ clearMocks配置选项可用于在测试之间自动清除模拟。”。您可以设置clearkMocks: truejest package.json配置。facebook.github.io/jest/docs/en/mock-function-api.html
科尔(Cole)

2
如果这是更改全局状态的问题,则始终可以将原始功能存储在某种测试变量中,并在测试后将其恢复
bobu

1
const original; beforeAll(()=> {original = m.otherFn; m.otherFn = jest.fn();})afterAll(()=> {m.otherFn = original;})它应该可以工作,但是我没有测试它
bobu

非常感谢!这解决了我的问题。
SlobodanKrasavčević

25

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

testFn只需otherFn 使用模块进行调用。

如果testFn使用模块调用,otherFn则可以对模块导出进行模拟otherFntestFn调用该模拟。


这是一个工作示例:

myModule.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
  });
});

2
这实质上是在Facebook的使用,并通过Facebook的开发在中间描述的方法的ES6版本的这篇文章
布赖恩·亚当斯

而不是将myModule导入自身,只需调用exports.otherFn()
andrhamm '18

3
@andrhammexports在ES6中不存在。exports.otherFn()由于ES6正在被编译为较早的模块语法,因此调用现在可以正常进行,但是当本地支持ES6时它将中断。
Brian Adams

现在有这个确切的问题,我敢肯定我曾经遇到过这个问题。我必须删除一堆export。<methodname>来帮助树摇晃,它破坏了许多测试。我会看看这是否有效果,但是看起来很hacky。我已经多次遇到这个问题,就像其他答案所说的那样,比如babel-plugin- rewire或什至更好的东西,npmjs.com / package / rewiremock,我相当确定也可以做到这一点。
Astridax '18年

是否有可能代替模拟返回值而模拟抛出?编辑:您可以,这是stackoverflow.com/a/50656680/2548010
巨额资金

10

转译的代码将不允许babel检索所otherFn()引用的绑定。如果使用函数expession,则应该能够实现嘲讽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');
  }),
}));

1
如果其他几个模块使用otherFn和testFn怎么办?您是否需要在所有使用这两个模块的测试文件中设置http模拟(无论深度如何)?另外,如果您已经有一个testFn的测试,为什么不直接在使用testFn的模块中将testFn而不是http存根?
ricked

1
因此,如果otherFn损坏,将使所有依赖于该测试的测试失败。另外,otherFn如果内部有5个if,则可能需要测试testFn所有这些子情况下的工作是否正常。您将有更多的代码路径可供测试。
Totty.js

6

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

对于模块:

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

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


2

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

myModule.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
  });
});

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.