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

NestJS健康检查控制器单元测试HttpModule mock及偶现失败问题

问题根因

测试偶现失败、mock不生效有两个核心原因:

  • 控制器存在实例级缓存逻辑:healthExpiry、isHealthy、healthCheckResult三个变量会缓存30秒内的健康检查结果,测试时如果命中缓存分支,根本不会触发HTTP请求逻辑,完全靠实例初始化的时间差决定走哪条分支,自然会出现随机过/失败的情况。
  • HTTP层mock不符合依赖要求:原有代码mock的是底层HttpService,但返回值结构不符合HttpHealthIndicator的校验规则(HttpHealthIndicator要求Axios响应的status字段为2xx数字类型状态码,原有mock返回的是字符串'ok'),且mock逻辑写死在beforeEach中,无法灵活切换服务正常/宕机场景。

最优实现方案

单元测试只需要验证控制器本身的逻辑,不需要验证@nestjs/terminus自带的pingCheck能力是否正常,因此直接mock控制器直接依赖的HttpHealthIndicator即可,比mock底层HttpService更稳定,不会因为Terminus库内部实现变更导致测试失效。
同时需要在每个测试用例执行前主动重置缓存状态,强制触发健康检查逻辑,彻底消除偶现问题。


修正后的测试代码
import { Test, TestingModule } from '@nestjs/testing';
import { HealthController } from './health.controller';
import { ConfigModule } from '@nestjs/config';
import MockEnvConfig from '../../test/resources/MockEnvConfig';
import { AppConfigModule } from '@app-config/app-config.module';
import { TerminusModule, HealthCheckService, HttpHealthIndicator } from '@nestjs/terminus';
import { InternalServerErrorException } from '@nestjs/common';
import * as moment from 'moment';

jest.mock('@rooster/node-services');

describe('HealthController', () => {
  let controller: HealthController;
  let httpHealthIndicator: jest.Mocked<HttpHealthIndicator>;

  beforeEach(async () => {
    // 重置所有mock状态
    jest.clearAllMocks();
    
    const module: TestingModule = await Test.createTestingModule({
      imports: [
        ConfigModule.forRoot({
          ignoreEnvVars: true,
          ignoreEnvFile: true,
          load: [MockEnvConfig],
        }),
        AppConfigModule,
        TerminusModule,
      ],
      controllers: [HealthController],
      providers: [
        // 直接mock控制器依赖的HttpHealthIndicator,跳过底层HTTP实现
        {
          provide: HttpHealthIndicator,
          useValue: {
            pingCheck: jest.fn(),
          },
        },
      ],
    }).compile();

    controller = module.get<HealthController>(HealthController);
    httpHealthIndicator = module.get(HttpHealthIndicator);
  });

  it('should be defined', () => {
    expect(controller).toBeDefined();
  });

  describe('checkHealth()', () => {
    // 每个用例执行前强制重置缓存,触发实际健康检查逻辑,消除偶现
    beforeEach(() => {
      controller['healthExpiry'] = moment().subtract(1, 'second');
      controller['isHealthy'] = false;
      controller['healthCheckResult'] = null;
    });

    it('服务正常时返回ok状态与健康检查结果', async () => {
      const mockPingResult = { 'payments-service': { status: 'up' } };
      httpHealthIndicator.pingCheck.mockResolvedValue(mockPingResult);

      const expectedResult = {
        status: 'ok',
        info: mockPingResult,
        error: {},
        details: mockPingResult,
      };

      const result = await controller.check();
      expect(result).toEqual(expectedResult);
      expect(controller['isHealthy']).toBe(true);
      expect(httpHealthIndicator.pingCheck).toHaveBeenCalledTimes(1);
      expect(httpHealthIndicator.pingCheck).toHaveBeenCalledWith(
        'payments-service',
        expect.stringContaining('/health')
      );
    });

    it('payments-service宕机时抛出500错误', async () => {
      const mockError = new Error('connect ECONNREFUSED');
      httpHealthIndicator.pingCheck.mockRejectedValue(mockError);

      await expect(controller.check()).rejects.toThrow(InternalServerErrorException);
      expect(controller['isHealthy']).toBe(false);
      expect(httpHealthIndicator.pingCheck).toHaveBeenCalledTimes(1);
    });

    it('缓存有效期内不重复发起健康检查请求', async () => {
      // 先执行一次检查设置缓存
      httpHealthIndicator.pingCheck.mockResolvedValue({ 'payments-service': { status: 'up' } });
      await controller.check();
      expect(httpHealthIndicator.pingCheck).toHaveBeenCalledTimes(1);

      // 立即第二次调用,应该命中缓存不发起请求
      await controller.check();
      expect(httpHealthIndicator.pingCheck).toHaveBeenCalledTimes(1);
    });
  });
});

方案说明
  • 彻底解决偶现问题:每个用例主动将healthExpiry设置为过去时间,强制走实际检查逻辑,完全规避时间差、实例状态共享导致的随机结果。
  • mock更稳定:直接mock控制器注入的HttpHealthIndicator,不需要关心Terminus底层是调用HttpService.get还是request方法,也不需要匹配Axios响应结构,库升级也不会导致测试失效。
  • 场景覆盖完整:除了服务正常、宕机两个场景,还可以覆盖缓存逻辑的测试,完全匹配控制器的实际业务逻辑。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.26 15:18:13