我有以下模块正在尝试在 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
提取到一个单独的模块中并模拟它。
在 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) 要求真实模块并在导出之前使用模拟函数对其进行修改可能很有用。
看起来我参加这个聚会迟到了,但是是的,这是可能的。
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
});
});
exports.otherFn()
exports
在 ES6 中不存在。调用 exports.otherFn()
现在可以工作,因为 ES6 正在被编译为更早的模块语法,但是当 ES6 被原生支持时它会中断。
import m from '../myModule';
对我不起作用,我确实使用过:
import * as m from '../myModule';
m.otherFn = jest.fn();
clearkMocks: true
jest package.json 配置。 facebook.github.io/jest/docs/en/mock-function-api.html
我知道很久以前有人问过这个问题,但我只是遇到了这种情况,终于找到了一个可行的解决方案。所以我想我会在这里分享。
对于模块:
// 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
的行为而不模拟自身。
转译后的代码将不允许 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
被破坏,所有依赖于该测试的测试都将失败。此外,如果 otherFn
内部有 5 个 if,您可能需要测试您的 testFn
是否适用于所有这些子案例。您现在将有更多的代码路径要测试。
基于 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
) 的路径(使其相对于包含 mockModulePartially
的 helpers.ts
)。作为第二个参数传递给 mockModulePartially
的 mocksCreator
函数应返回模块的模拟。此函数接收 originalModule
并且模拟实现可以选择依赖它。
我通过在这里找到的各种答案解决了我的问题:
我的模块.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
});
});
除了这里的第一个答案,您还可以使用 babel-plugin-rewire 来模拟导入的命名函数。您可以从表面上查看 named function rewiring 部分。
对于您的情况,其中一个直接好处是您无需更改从函数中调用其他函数的方式。
return
语句并将箭头函数体包装在括号中来使其更加简洁:例如。jest.mock('./myModule', () => ({ ...jest.requireActual('./myModule'), otherFn: () => {}}))
getCurrentMessage
。...jest.requireActual
不适用于我,因为我有使用 babel 的路径别名.. 可以与...require.requireActual
一起使用,也可以在从路径中删除别名后使用testFn
,而应调用otherFn
的模拟。如果您调用testFn
,它仍将使用原始otherFn
。至少,它在 TypeScript 中对我不起作用。请参阅 explaination。