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

如何在NestJS单元测试中优雅退出模拟的CronJob

如何在NestJS单元测试中优雅退出模拟的CronJob

我完全懂你遇到的这个困扰——在给CronService写单元测试时,加了Mock CronJob之后,测试跑完总是弹出“worker进程未优雅退出”的提示,用--detectOpenHandles也查不出头绪对吧?咱们来一步步解决这个问题:

问题根源

你的CronService在构造函数里就自动调用了addCronJob,这个方法内部会创建真实的CronJob实例并调用start()启动它。哪怕你Mock了SchedulerRegistry,但实际运行的定时任务并没有在测试结束后被停止,后台一直挂着,导致Node.js进程没法正常退出。另外你定义的mockCronJob实例如果没被显式停止,也可能是个小隐患。

解决方案:测试后显式清理所有定时任务

核心思路就是:在每个测试用例结束后,手动停止所有启动的CronJob实例,不管是服务内部创建的,还是你Mock的那个。下面结合你的代码给出具体修改方案:

1. 给CronService添加清理方法(可选但推荐)

首先在你的CronService里加一个用于停止所有定时任务的方法,方便测试时调用:

import { Injectable, Logger } from '@nestjs/common';
import { SchedulerRegistry } from '@nestjs/schedule';
import { CronJob } from 'cron';
import { PriceService } from '../price/price.service';

@Injectable()
export class CronService {
  constructor(private schedulerRegistry: SchedulerRegistry, private priceService: PriceService) {
    this.addCronJob('startup',1)
  }

  private readonly logger = new Logger(CronService.name);

  // 新增:停止所有定时任务的清理方法
  stopAllCronJobs(): void {
    try {
      const priceUpdateJob = this.schedulerRegistry.getCronJob('priceUpdateTimer');
      if (priceUpdateJob) {
        priceUpdateJob.stop();
        this.schedulerRegistry.deleteCronJob('priceUpdateTimer');
        this.logger.log('Stopped priceUpdateTimer cron job');
      }
    } catch (error) {
      this.logger.warn('No priceUpdateTimer cron job to stop');
    }
  }

  addCronJob(name: string, minutes: number): number {
    let priceUpdateTimer = 'priceUpdateTimer'
    if(0 >= minutes || minutes >= 60) {
      this.logger.error(`Tried updating the priceUpdatetimer to ${minutes} minutes.`);
      return 0;
    }
    if(name === priceUpdateTimer || name == 'startup') {
      if(name === priceUpdateTimer && this.getCronByName(priceUpdateTimer)) {
        this.deleteCron(priceUpdateTimer);
      }
      let cronString = `*/${minutes} * * * *`;
      const priceUpdateJob = new CronJob(cronString, () => {
        this.logger.log('Updating prices.')
        this.priceService.updatePrices();
      });
      this.schedulerRegistry.addCronJob(priceUpdateTimer, priceUpdateJob);
      priceUpdateJob.start();
      this.logger.log(`Changed the priceUpdatetimer to ${minutes} minutes.`);
      return minutes;
    }
    return 0;
  }

  deleteCron(name: string) {
    this.schedulerRegistry.deleteCronJob(name);
    this.logger.warn(`job ${name} deleted!`);
  }

  getCronByName(name: string): number {
    try {
      const job = this.schedulerRegistry.getCronJob(name);
      let minutes: number = Number.parseInt(job.cronTime.source.toString().replace(/\D/g, ""));
      return minutes;
    } catch {
      this.logger.warn(`No cron job found by name ${name}`);
      return 0;
    }
  }
}

2. 修改测试文件,添加清理逻辑

在测试文件里,我们需要:

  • 每个测试前重新创建Mock CronJob,避免跨测试污染
  • 添加afterEach钩子,调用服务的清理方法,同时停止Mock的CronJob

修改后的测试代码:

import { Test, TestingModule } from '@nestjs/testing';
import { CronService } from './cron.service';
import { SchedulerRegistry } from '@nestjs/schedule';
import { CronJob } from 'cron';
import { PriceModule } from '../price/price.module';

describe('CronService', () => {
  let service: CronService;
  let mockCronJob: CronJob;
  let mockSchedulerRegistry: jest.Mocked<SchedulerRegistry>;

  beforeEach(async () => {
    // 每次测试都新建一个Mock CronJob
    mockCronJob = new CronJob('*/5 * * * *', () => {});

    // 改进Mock SchedulerRegistry,能追踪添加的Job
    mockSchedulerRegistry = {
      addCronJob: jest.fn((name, job) => {
        // 记录添加的Job,方便后续停止
        if (name === 'priceUpdateTimer') {
          mockCronJob = job;
        }
      }),
      getCronJob: jest.fn().mockReturnValue(mockCronJob),
      deleteCronJob: jest.fn(),
    } as unknown as jest.Mocked<SchedulerRegistry>;

    const module: TestingModule = await Test.createTestingModule({
      providers: [
        CronService,
        { provide: SchedulerRegistry, useValue: mockSchedulerRegistry }
      ],
      imports: [PriceModule]
    }).compile();

    service = module.get<CronService>(CronService);
  });

  // 测试结束后清理所有定时任务
  afterEach(async () => {
    // 停止服务里的定时任务
    service.stopAllCronJobs();
    // 停止Mock的CronJob
    if (mockCronJob) {
      mockCronJob.stop();
    }
  });

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

额外提示

  • 如果你不想给CronService加清理方法,也可以直接通过Mock的SchedulerRegistry获取到添加的CronJob实例,然后在afterEach里调用stop()
  • 确保所有调用了start()的CronJob都被stop(),这是解决进程无法优雅退出的关键——因为CronJob启动后会占用事件循环,不停止的话Node.js进程不会自动退出

内容来源于stack exchange

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.04.08 12:04:52