You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

当装饰器可能阻止方法执行时,如何用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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.09 12:05:11