如何在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
相关产品推荐
相关产品推荐

