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

如何对NestJS自定义CacheInterceptor进行单元测试

Nest自定义CacheInterceptor单元测试实现方案

测试核心思路是:你只需要mock拦截器依赖的内置属性、构造符合入参要求的ExecutionContext mock对象,即可完成trackBy方法的全逻辑覆盖,不需要启动完整的Nest测试容器。

核心依赖Mock说明

你的自定义拦截器只用到了两个继承自父类的属性,以及ExecutionContext入参的两个方法,所有mock都只需要实现用到的部分即可,不需要补全全量接口:

  • reflector:仅需mockget方法,返回预设的缓存元数据值
  • httpAdapterHost:仅需在其httpAdapter属性上挂载mock的httpAdapter,后者实现getRequestUrl方法返回预设请求路径即可
  • ExecutionContext:仅需实现getHandler(返回模拟的路由处理函数)、switchToHttp(返回带getRequest方法的对象,getRequest返回构造的mock请求对象,包含body和headers字段)两个方法,剩余接口方法不需要实现,用TS类型断言绕过类型检查即可。

测试覆盖场景建议

为保证逻辑符合预期,需要覆盖以下场景:

  • 所有hash因子(缓存元数据、请求URL、请求体、Accept-Language头)都存在时,缓存键计算正确
  • 任意一个hash因子变化时,生成的缓存键会同步变化
  • 部分因子不存在(比如无缓存元数据、GET请求无body、请求未带Accept-Language头)时,不会抛出异常,可正常生成缓存键

完整测试代码(Jest)

import { MyCacheInterceptor } from './my-cache.interceptor';
import { CACHE_KEY_METADATA, ExecutionContext } from '@nestjs/common';
import { createHash } from 'crypto';

describe('MyCacheInterceptor', () => {
  let interceptor: MyCacheInterceptor;
  // mock依赖对象,只实现用到的方法
  const mockReflector = {
    get: jest.fn()
  };
  const mockHttpAdapter = {
    getRequestUrl: jest.fn()
  };
  const mockHttpAdapterHost = {
    httpAdapter: mockHttpAdapter
  };
  // 构造函数需要传入cacheManager,传空对象mock即可,测试trackBy不会用到
  const mockCacheManager = {};

  beforeEach(() => {
    // 重置所有mock调用记录
    jest.clearAllMocks();
    // 实例化拦截器,传入构造函数所需的三个依赖
    interceptor = new MyCacheInterceptor(
      mockCacheManager as any,
      mockReflector as any,
      mockHttpAdapterHost as any
    );
  });

  /**
   * 构造mock ExecutionContext的工具函数
   * 不同测试用例可传入不同参数模拟不同请求场景
   */
  const buildMockContext = (options: {
    requestUrl: string;
    body?: any;
    acceptLanguage?: string;
  }): ExecutionContext => {
    const mockRequest = {
      body: options.body,
      headers: {
        'accept-language': options.acceptLanguage
      }
    };
    return {
      getHandler: jest.fn().mockReturnValue(() => {}),
      switchToHttp: jest.fn().mockReturnValue({
        getRequest: jest.fn().mockReturnValue(mockRequest)
      })
    } as unknown as ExecutionContext;
  };

  // 对齐拦截器hash逻辑,计算预期缓存键
  const calcExpectedKey = (factors: (string | undefined)[]) => {
    return factors.reduce((hash, item) => {
      return hash.update(item ? Buffer.from(item) : Buffer.alloc(0));
    }, createHash('md5')).digest('hex');
  };

  it('所有hash因子存在时,可正确生成缓存键', () => {
    const testCacheMeta = 'user_list_cache';
    const testUrl = '/api/users';
    const testBody = { page: 1, pageSize: 10 };
    const testLang = 'zh-CN';

    // 预设mock返回值
    mockReflector.get.mockReturnValue(testCacheMeta);
    mockHttpAdapter.getRequestUrl.mockReturnValue(testUrl);

    const context = buildMockContext({
      requestUrl: testUrl,
      body: testBody,
      acceptLanguage: testLang
    });

    const resultKey = interceptor.trackBy(context);
    const expectedKey = calcExpectedKey([
      testCacheMeta,
      testUrl,
      JSON.stringify(testBody),
      testLang
    ]);

    expect(resultKey).toBe(expectedKey);
    // 验证依赖方法被正确调用
    expect(mockReflector.get).toHaveBeenCalledWith(CACHE_KEY_METADATA, context.getHandler());
    expect(mockHttpAdapter.getRequestUrl).toHaveBeenCalledWith(context.switchToHttp().getRequest());
  });

  it('请求体变化时,生成的缓存键不同', () => {
    mockReflector.get.mockReturnValue('user_list_cache');
    mockHttpAdapter.getRequestUrl.mockReturnValue('/api/users');

    const ctx1 = buildMockContext({
      requestUrl: '/api/users',
      body: { page: 1 },
      acceptLanguage: 'zh-CN'
    });
    const ctx2 = buildMockContext({
      requestUrl: '/api/users',
      body: { page: 2 },
      acceptLanguage: 'zh-CN'
    });

    expect(interceptor.trackBy(ctx1)).not.toBe(interceptor.trackBy(ctx2));
  });

  it('Accept-Language头变化时,生成的缓存键不同', () => {
    mockReflector.get.mockReturnValue('user_list_cache');
    mockHttpAdapter.getRequestUrl.mockReturnValue('/api/users');

    const ctx1 = buildMockContext({
      requestUrl: '/api/users',
      body: { page: 1 },
      acceptLanguage: 'zh-CN'
    });
    const ctx2 = buildMockContext({
      requestUrl: '/api/users',
      body: { page: 1 },
      acceptLanguage: 'en-US'
    });

    expect(interceptor.trackBy(ctx1)).not.toBe(interceptor.trackBy(ctx2));
  });

  it('无缓存元数据、无请求体、无Accept-Language头时,可正常生成缓存键', () => {
    const testUrl = '/api/public/config';
    mockReflector.get.mockReturnValue(undefined);
    mockHttpAdapter.getRequestUrl.mockReturnValue(testUrl);

    const context = buildMockContext({
      requestUrl: testUrl
    });

    const resultKey = interceptor.trackBy(context);
    const expectedKey = calcExpectedKey([
      undefined,
      testUrl,
      JSON.stringify(undefined),
      undefined
    ]);

    expect(resultKey).toBe(expectedKey);
  });
});

注意事项

  • 不需要为了满足TS类型要求补全ExecutionContext的所有方法,测试场景下只需要保证实际调用到的方法存在即可,类型断言是单测里的常规操作。
  • 父类CacheInterceptor自带的缓存读写、响应拦截逻辑属于Nest框架官方维护范畴,已经过框架层测试,不需要在业务单测中重复覆盖,只需要验证自己重写的trackBy逻辑即可。
  • 不要硬编码md5结果作为预期值,用和业务逻辑一致的辅助函数计算预期值,后续如果hash因子调整,只需要同步修改辅助函数的入参即可,降低测试维护成本。

内容的提问来源于stack exchange,提问作者sclausen

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.27 15:09:22