当模块被取消模拟时,如何在 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()
});
});

任何帮助/见解都是值得感激的。

175070 次浏览

经过翻译的代码将不允许 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');
}),
}));
import m from '../myModule';

不适合我,我用过:

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


m.otherFn = jest.fn();

除了这里的第一个答案之外,您还可以使用 巴别尔-插件-重新连接来模拟导入的命名函数。您可以从表面上查看这个部分 命名函数重新布线

这种情况的一个直接好处是,您不需要改变从函数调用其他函数的方式。

看来我要迟到了,不过是的,这是有可能的。

只需要调用 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
});
});

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’s Manual Mocks 文档(接近 < em > 例子 的末尾)中也引用了这个方法:

为了确保手动模拟和它的实际实现保持同步,在导出之前,可能需要在手动模拟中使用 jest.requireActual(moduleName)并使用模拟函数修改实际模块。

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

对于模块:

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


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

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

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

基于 Brian Adams 的回答,这就是我能够在 TypeScript 中使用相同方法的原因。此外,使用 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');
});
});

测试/辅助人员

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

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