我有一个正在尝试运行的测试,我想使用 jest.mock 来模拟一个模块。但是,我似乎无法让这个工作,在下面的示例中,始终调用原始的
serializeProduct
方法。我尝试过间谍,我尝试过嘲笑该模块。当我注销作为模拟/间谍的变量时,它们确实是模拟函数,至少因此模拟正在发生。只是有些东西导致它调用原始方法而不是模拟方法。
产品.序列化器.ts
import { ProductDto } from './product.dto';
export function serializeProducts(value: any[]): ProductDto[] {
return value.map(serializeProduct);
}
export function serializeProduct(value: any) {
return value;
}
product.serializer.spect.ts 与上述位于同一文件夹中。
import { expect } from '@jest/globals';
import * as productsSerializer from './products.serializer';
describe('serializer', () => {
afterEach(() => {
jest.clearAllMocks();
jest.restoreAllMocks();
});
describe('serializeProducts method', () => {
it('should foo', () => {
const packages = [1,2,3,];
const spy = jest.spyOn(productsSerializer, 'serializeProduct').mockImplementation(() => undefined);
productsSerializer.serializeProducts(packages);
expect(spy).toHaveBeenCalledTimes(3);
});
});
});
我也尝试过像这样的模块模拟
jest.mock('./products.serializer', () => ({
...jest.requireActual('./products.serializer'),
serializeProduct: jest.fn()
});
import { expect } from '@jest/globals';
import * as productsSerializer from './products.serializer';
describe('serializer', () => {
afterEach(() => {
jest.clearAllMocks();
jest.restoreAllMocks();
});
describe('serializeProducts method', () => {
it('should foo', () => {
const packages = [1,2,3,];
(productsSerializer.serializeProduct as jest.Mock).mockImplementation(() => undefined);
productsSerializer.serializeProducts(packages);
expect(productsSerializer.serializeProduct).toHaveBeenCalledTimes(3);
});
});
});
我还单独导入了上例中的方法,而不是导入
* as
没有效果。
我在我正在从事的同一个项目中找到了这方面的例子,但这个却没有。
编辑 针对建议的其他问题所接受的解决方案是不可行的,因为它需要更改应用程序代码来修复测试。
另一个最高的解决方案是移动导出 - 同时更改应用程序代码。我已经确认,如果我将其中一种方法移出,它确实有效,但这实际上也不应该是一个可行的解决方案。它可能对某些人有用,但在我的应用程序上下文中这样做没有意义。
所以我想找出为什么这不起作用。如果它应该有效,那么应该有一个不涉及更改应用程序代码的解决方案。
编辑2 我发现将方法转换为导出常量并将它们定义为箭头函数可以解决这个问题。这意味着它可能与 jest/node 导入有关。
您在嘲笑
serializeProduct
时遇到困难,因为 serializeProducts
正在从同一模块内调用它。这个问题的根本原因是代码的转译方式。
让我们看一下下面的例子:
export function foo() {};
export function bar() { foo() };
你的转译器很可能会将其转换成这样的东西(简化的):
function foo() {};
function bar() { foo() };
exports.foo = foo;
exports.bar = bar;
这里的问题是,我们在测试中嘲笑(重新分配)
exports.foo
,而bar
正在调用原始的foo
。
一个解决方案 因此,我们可以做的一件事就是调用导出的
foo
属性,又名 exports.foo
,这样当它被模拟时,我们就调用模拟。
export function foo() {};
export function bar() { exports.foo() };
这是有效的代码,将被正确转译,但它很难看。
为您提供更好的解决方案
既然你使用的是 Typescript,那么你很有可能使用
ts-jest
作为你的转换器。 ts-jest
有一个不错的小怪癖,它将导出的 variables 替换为其 exports
成员对应项。
因此,我们不需要使用函数语句来调用我们定义的函数,我们只需要调用一个分配给我们想要的函数的变量,并且由于ts-jest
的有用替换,该变量将是可模拟的。我们可以通过不同的方式做到这一点:
function foo () {};
// All 3 of these are replaced by `exports.<name>` by `ts-jest` when they're called
const foo1 = foo; // Reassign to a new variable
const foo2 = function() {} // Anonymous function
const foo3 = () => {} // Arrow function
这是我通过调试本地运行的测试得到的快速说明。让我们看看原来的模块,以及它是如何被ts-jest
改造的。
// Original
export function foo() {};
export const bar = function() {}
export function buzz() {
foo();
bar();
}
// Transformed
Object.defineProperty(exports, "__esModule", { value: true });
exports.bar = void 0;
exports.foo = foo;
exports.buzz = buzz;
function foo() {};
const bar = () => {};
exports.bar = bar;
function buzz() {
foo();
(0, exports.bar)();
}
注意 bar
的函数调用如何被其
exports
对应函数替换。现在我们可以毫无问题地使用
spyOn
。
荣誉奖 - 依赖注入
对于您的特定情况,serializeProducts
本质上是
Array.map
的包装。您可以给它第二个参数,默认传递它
serializeProduct
,然后在测试中向它传递一个模拟函数。它为
serializeProducts
提供了额外的灵活性,而无需过多改变整体工作流程,并且使测试变得更加容易。