当装饰器可能阻止方法执行时,如何用Jest监视TypeScript装饰方法?
问题:Jest如何测试被装饰器拦截的方法是否实际执行?
在TypeScript项目中使用装饰器增强方法行为时,比如下面的CacheDecorator会根据缓存情况决定是否执行原始方法:
class TestClass { @CacheDecorator async methodToTest(arg1: string, arg2: number) { return { arg1, arg2 }; } }
CacheDecorator逻辑:若缓存存在则直接返回缓存值,不执行methodToTest;缓存不存在时才允许原始方法运行。但直接用jest.spyOn()监视时,无法区分装饰器包装的方法和原始方法的调用情况,如何有效验证原始方法是否被执行?
解决方案
方法1:抽离原始方法逻辑到独立函数
把methodToTest的核心业务逻辑抽成单独的函数,在类方法中直接调用这个函数。这样可以直接监视这个独立函数的调用情况,完全绕过装饰器的干扰。
代码改造:
// 抽离核心逻辑为独立函数 export const methodToTestImpl = async (arg1: string, arg2: number) => { return { arg1, arg2 }; }; class TestClass { @CacheDecorator async methodToTest(arg1: string, arg2: number) { // 调用抽离后的逻辑 return methodToTestImpl(arg1, arg2); } }
测试代码:
import { TestClass, methodToTestImpl } from './your-file'; import { clearCache } from './cache-utils'; // 假设你有清空缓存的工具函数 test('缓存不存在时,原始逻辑函数被调用', async () => { // 清空缓存确保不命中 await clearCache(); // 监视抽离的逻辑函数 const spy = jest.spyOn(require('./your-file'), 'methodToTestImpl').mockResolvedValue({ arg1: 'test', arg2: 1 }); const instance = new TestClass(); await instance.methodToTest('test', 1); expect(spy).toHaveBeenCalledTimes(1); // 恢复原始函数 spy.mockRestore(); }); test('缓存存在时,原始逻辑函数不被调用', async () => { // 先调用一次填充缓存 const instance = new TestClass(); await instance.methodToTest('test', 1); // 监视抽离的逻辑函数 const spy = jest.spyOn(require('./your-file'), 'methodToTestImpl').mockResolvedValue({ arg1: 'test', arg2: 1 }); // 再次调用,应该命中缓存 await instance.methodToTest('test', 1); expect(spy).not.toHaveBeenCalled(); spy.mockRestore(); });
方法2:修改装饰器,暴露原始方法引用
如果是自己实现的CacheDecorator,可以在装饰器内部把原始方法挂载到包装后的方法属性上,方便测试时直接访问并监视。
改造装饰器:
function CacheDecorator(target: any, propertyKey: string, descriptor: PropertyDescriptor) { const originalMethod = descriptor.value; // 将原始方法挂载到包装函数的属性上 (descriptor.value as any).originalMethod = originalMethod; // 装饰器原有逻辑 descriptor.value = async function(...args: any[]) { const cacheKey = `${propertyKey}:${JSON.stringify(args)}`; const cachedValue = await getCache(cacheKey); if (cachedValue) { return cachedValue; } const result = await originalMethod.apply(this, args); await setCache(cacheKey, result); return result; }; return descriptor; }
测试代码:
import { TestClass } from './your-file'; import { clearCache } from './cache-utils'; test('缓存不存在时执行原始方法', async () => { await clearCache(); const instance = new TestClass(); // 获取原始方法引用 const originalMethod = (TestClass.prototype.methodToTest as any).originalMethod; // 替换实例方法为一个会调用原始方法的spy const spy = jest.spyOn(instance, 'methodToTest').mockImplementation(async (...args) => { return originalMethod.apply(instance, args); }); await instance.methodToTest('test', 1); expect(spy).toHaveBeenCalled(); spy.mockRestore(); }); test('缓存存在时不执行原始方法', async () => { // 先填充缓存 const instance = new TestClass(); await instance.methodToTest('test', 1); // 监视原始方法 const originalMethod = (TestClass.prototype.methodToTest as any).originalMethod; const spy = jest.spyOn(instance, 'methodToTest').mockImplementation(async (...args) => { return originalMethod.apply(instance, args); }); await instance.methodToTest('test', 1); expect(spy).not.toHaveBeenCalled(); spy.mockRestore(); });
方法3:Mock装饰器,临时禁用包装逻辑
在测试中直接Mock掉CacheDecorator,让它不做任何包装,直接返回原始方法。这样可以单独测试原始方法的行为,再单独测试装饰器的缓存逻辑。
测试代码:
// 先Mock装饰器,让它不生效 jest.mock('./cache-decorator', () => ({ CacheDecorator: (target: any, key: string, descriptor: PropertyDescriptor) => descriptor })); // 导入TestClass,此时装饰器已被Mock import { TestClass } from './your-file'; test('原始方法可正常被调用', async () => { const instance = new TestClass(); const spy = jest.spyOn(instance, 'methodToTest'); await instance.methodToTest('test', 1); expect(spy).toHaveBeenCalledTimes(1); spy.mockRestore(); }); // 另一个测试块,不Mock装饰器,测试缓存逻辑 describe('带CacheDecorator的测试', () => { import { TestClass } from './your-file'; import { clearCache } from './cache-utils'; test('缓存命中时不执行原始方法', async () => { await clearCache(); const instance = new TestClass(); // 第一次调用填充缓存 await instance.methodToTest('test', 1); // 替换原始方法为一个会抛出错误的spy(如果被调用则测试失败) const spy = jest.spyOn(TestClass.prototype, 'methodToTest').mockImplementation(() => { throw new Error('原始方法被调用了!'); }); // 第二次调用应该命中缓存,不会触发错误 await expect(instance.methodToTest('test', 1)).resolves.not.toThrow(); spy.mockRestore(); }); });
内容的提问来源于stack exchange,提问作者Rems
相关产品推荐
相关产品推荐

