Jest无法正确Mock TypeScript模块问题排查
Jest Mock TypeScript模块失效:原方法始终被调用
问题概述
测试TypeScript模块时,尝试用jest.spyOn或jest.mockMock同模块内的serializeProduct方法,但实际执行serializeProducts时仍会调用原方法,即使日志显示Mock函数已生成。项目中其他同类Mock正常,仅该案例失败。已知修改业务代码(如改为箭头函数常量)可解决,但业务代码不可修改,也无法调整导出顺序。
相关代码
业务代码(product.serializer.ts)
import { ProductDto } from './product.dto'; export function serializeProducts(value: any[]): ProductDto[] { return value.map(serializeProduct); } export function serializeProduct(value: any) { return value; }
测试尝试1(spyOn方式)
import { expect } from '@jest/globals'; import * as productsSerializer from './products.serializer'; describe('serializer', () => { afterEach(() => { jest.clearAllMocks(); jest.restoreAllMocks(); }); describe('serializeProducts method', () => { it('should call serializeProduct 3 times', () => { const packages = [1,2,3]; const spy = jest.spyOn(productsSerializer, 'serializeProduct').mockImplementation(() => undefined); productsSerializer.serializeProducts(packages); expect(spy).toHaveBeenCalledTimes(3); }); }); });
测试尝试2(模块Mock方式)
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 call serializeProduct 3 times', () => { const packages = [1,2,3]; (productsSerializer.serializeProduct as jest.Mock).mockImplementation(() => undefined); productsSerializer.serializeProducts(packages); expect(productsSerializer.serializeProduct).toHaveBeenCalledTimes(3); }); }); });
问题原因
核心在于模块内部函数的引用机制:
serializeProducts内部直接调用同模块的serializeProduct时,引用的是模块内部的原始函数实例,而非导出对象上的函数引用。- Jest的
jest.spyOn和jest.mock仅能修改模块导出对象上的函数引用,无法影响模块内部直接调用的原始函数。 - 改成箭头函数常量后生效,是因为箭头函数作为导出的常量,模块内部调用时会通过导出对象间接引用,此时Mock修改导出对象的引用就能生效。
无需修改业务代码的解决方案
方案:重新Mock模块内的serializeProducts,让它调用导出的Mock函数
在jest.mock中重新实现serializeProducts,使其调用导出的serializeProduct(即我们的Mock函数),而非模块内部的原始函数:
jest.mock('./products.serializer', () => { const actual = jest.requireActual('./products.serializer'); return { ...actual, serializeProduct: jest.fn(), // 重新实现serializeProducts,让它调用当前模块导出的serializeProduct serializeProducts: (value: any[]) => value.map(item => (module.exports as any).serializeProduct(item)) }; }); import { expect } from '@jest/globals'; import * as productsSerializer from './products.serializer'; describe('serializer', () => { afterEach(() => { jest.clearAllMocks(); }); describe('serializeProducts method', () => { it('should call serializeProduct exactly 3 times', () => { const packages = [1,2,3]; (productsSerializer.serializeProduct as jest.Mock).mockReturnValue(undefined); productsSerializer.serializeProducts(packages); expect(productsSerializer.serializeProduct).toHaveBeenCalledTimes(3); }); }); });
此方案通过在Mock中重写serializeProducts,让它间接引用导出的serializeProduct,从而使Mock能够生效,完全不需要修改业务代码。
内容的提问来源于stack exchange,提问作者kamcknig
相关产品推荐
相关产品推荐

