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

时间:2016-09-28 18:48:31

标签: unit-testing ecmascript-6 jestjs

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

感谢任何帮助/见解。

8 个答案:

答案 0 :(得分:22)

import m from '../myModule';

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

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

m.otherFn = jest.fn();

答案 1 :(得分:19)

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

  

jest.requireActual(moduleName)

     

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

示例

我更喜欢这种简洁的用法,在您需要并分散在返回的对象中的情况下

// myModule.test.js

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

describe(...)

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

  

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

答案 2 :(得分:10)

转换后的代码不允许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');
  }),
}));

答案 3 :(得分:8)

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

testFn只需使用模块调用otherFn

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


这是一个有效的示例:

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

答案 4 :(得分:0)

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

这里遇到的直接好处之一就是您不需要更改从函数调用另一个函数的方式。

答案 5 :(得分:0)

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

对于模块:

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

您的模拟现在应该可以正常运行了。

答案 6 :(得分:0)

我通过在这里找到的答案混合起来解决了我的问题:

myModule.js

cache_size

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

答案 7 :(得分:0)

基于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 };

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

test / 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)的路径(使其相对于包含helpers.ts的{​​{1}})。作为第二个参数传递给mockModulePartially的{​​{1}}函数应返回模块的模拟。此函数接收mocksCreator,并且模拟实现可以选择依赖它。