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

Jon*_*ins 61 unit-testing ecmascript-6 jestjs

我有以下模块我试图在Jest中测试:

// myModule.js

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

export function testFn() {
  otherFn();

  // do other things
}
Run Code Online (Sandbox Code Playgroud)

如上图所示,这部分出口命名的功能和重要的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()
  });
});
Run Code Online (Sandbox Code Playgroud)

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

gfu*_*lam 42

jest.requireActual()在里面使用jest.mock()

jest.requireActual(moduleName)

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

例

我更喜欢这种简洁的用法,你需要并在以下内容中传播jest.requireActual(moduleName):

// myModule.test.js

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

describe(...)
Run Code Online (Sandbox Code Playgroud)

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

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

  • 这个答案实际上并没有回答问题。提供的解决方案不会调用“testFn”,而“testFn”又应该调用“otherFn”的模拟。如果你调用 `testFn` 它仍然会使用原来的 `otherFn`。至少,它在 TypeScript 中对我不起作用。请参阅[说明](https://github.com/facebook/jest/issues/936#issuecomment-214939935)。 (6认同)
  • 首先,您可以通过删除`return`语句并将箭头函数主体括在括号中来使其更加简洁:`jest.mock('./ myModule',()=>({... jest.requireActual('./ myModule'),otherFn:()=> {}}))) (4认同)
  • 这很棒!这应该是公认的答案。 (2认同)
  • 不适合我: jest.mock('../../src/helpers', () => ({ ...jest.requireActual('../../src/helpers'), getCurrentMessage: ( ): { getFrom: () => string } => { return { getFrom: (): string => '测试用户 <test.user@example.com>', }; }, })); 我正在测试的函数仍在调用实际的“getCurrentMessage”。 (2认同)
  • `...jest.requireActual` 不能正常工作,因为我使用 babel 进行路径别名。可以与 `...require.requireActual` 一起使用,也可以在从路径中删除别名后工作 (2认同)

bob*_*obu 28

import m from '../myModule';
Run Code Online (Sandbox Code Playgroud)

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

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

m.otherFn = jest.fn();
Run Code Online (Sandbox Code Playgroud)

  • 如何在测试后恢复otherFn的原始功能,以免干扰其他测试? (4认同)
  • Downvote因为这会污染全球状态 (3认同)
  • 如果这是改变全局状态的问题,您可以始终将原始功能存储在某种测试变量中,并在测试后将其恢复 (2认同)

Bri*_*ams 14

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

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;
}
Run Code Online (Sandbox Code Playgroud)

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
  });
});
Run Code Online (Sandbox Code Playgroud)

  • 这本质上是Facebook使用的方法的ES6版本,并由Facebook开发人员在[这篇文章]的中间进行了描述(https://github.com/facebook/jest/issues/936#issuecomment-214939935)。 (2认同)
  • @andrhamm`exports`在ES6中不存在。调用`exports.otherFn()`现在可以正常工作,因为ES6正在被编译为更早的模块语法,但是当本地支持ES6时它将中断。 (2认同)

vut*_*ran 9

转换后的代码不允许babel检索otherFn()所指的绑定.如果你使用功能探索,你应该能够实现嘲弄otherFn().

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

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

  // do other things
}
Run Code Online (Sandbox Code Playgroud)

 

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

m.otherFn = jest.fn();
Run Code Online (Sandbox Code Playgroud)

但正如@kentcdodds在之前的评论中所提到的,你可能不想嘲笑otherFn().相反,只需编写一个新的规范otherFn()并模拟它正在进行的任何必要的调用.

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

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

在这里,您可能希望根据模拟的http.get实现来模拟和更新断言.

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

  • 因此,如果“otherFn”损坏,则依赖于该测试的所有测试都会失败。另外,如果“otherFn”内部有 5 个 if,您可能需要测试您的“testFn”是否适用于所有这些子情况。现在您将有更多的代码路径需要测试。 (2认同)

Jac*_*sey 9

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

对于模块:

// myModule.js

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

export function testFn() {
  otherFn();

  // do other things
}
Run Code Online (Sandbox Code Playgroud)

您可以更改为以下内容:

// myModule.js

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

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

  // do other things
}
Run Code Online (Sandbox Code Playgroud)

将它们导出为常量而不是函数。我相信这个问题与 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
  });
});

Run Code Online (Sandbox Code Playgroud)

你的模拟现在应该像你通常期望的那样工作。


Ezz*_*zze 5

根据Brian Adams 的回答,这就是我能够在 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 };
Run Code Online (Sandbox Code Playgroud)

测试/模块.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');
  });
});
Run Code Online (Sandbox Code Playgroud)

测试/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) };
  });
}
Run Code Online (Sandbox Code Playgroud)

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