NestJS单元测试中Mock newrelic模块失效问题排查
NestJS 集成 NewRelic 单元测试报错解决方案
为什么测试环境下 NewRelic 无法找到对应配置文件
核心原因有三点:
- NewRelic Node Agent 初始化逻辑是:第一次被
import/require时会立刻执行初始化流程,默认从执行命令的工作目录(也就是跑npm run test时的项目根目录) 查找名为newrelic.js的配置文件,不会递归扫描/src这类子目录,也默认不识别.ts后缀的配置文件,找不到配置就会抛出未设置app_name的错误。 - 应用正常运行时,你在入口文件
main.ts最顶部先导入了/src/newrelic.ts自定义配置,之后才加载业务模块,NewRelic 能提前拿到配置参数所以运行正常。但跑单元测试时,Jest 会直接加载被测试的AccountsService文件,文件顶部直接导入了newrelic,这时候 NewRelic 还没拿到自定义配置就先触发了初始化逻辑。 - 你写的
jest.mock位置错误:写在describe块内部的 mock 不会被 Jest 提升到文件顶部执行,真实的 NewRelic 模块会在 mock 生效前就被加载初始化,自然走了默认的配置查找逻辑,触发报错。
Mock NewRelic 模块的正确实现方式
方案1:单文件内正确Mock(适配单个测试文件快速修复)
把jest.mock调用移到测试文件顶层,所有 import 语句之后、describe块之前。Jest 会自动把顶层的jest.mock提升到整个文件最顶部执行,确保所有业务模块导入 NewRelic 之前,就已经把模块替换为 Mock 版本,不会触发真实初始化:
// 先放所有业务相关的import import { getModelToken } from '@nestjs/mongoose'; import { Test, TestingModule } from '@nestjs/testing'; import { Account } from '../entities/account.entity'; import { AccountsController } from '../accounts.controller'; import { AccountsService } from '../accounts.service'; import { CreateAccountData } from '../dto/create-account-data'; // 顶层写jest.mock,不要放在describe里 jest.mock('newrelic', () => ({ recordMetric: jest.fn(), recordCustomEvent: jest.fn(), noticeError: jest.fn(), // 按需补全项目中用到的其他NewRelic方法 })); // mock注册完成后再导入newrelic,拿到的是mock实例 import newrelic from 'newrelic'; describe('AccountsController', () => { let controller: AccountsController; let service: AccountsService; const now = new Date(); const fakeAccount: Account = { _id: 'lalal', name: 'Palmeiras', createdAt: now, }; const postBody: CreateAccountData = { name: 'Palmeiras', }; class accountModel { constructor(private data) {} save = jest.fn().mockResolvedValue(this.data); static find = jest.fn().mockResolvedValue([fakeAccount]); static findOne = jest.fn().mockResolvedValue(fakeAccount); static create = jest.fn().mockResolvedValue(fakeAccount); static findOneAndUpdate = jest.fn().mockResolvedValue(fakeAccount); static deleteOne = jest.fn().mockResolvedValue(true); } beforeEach(async () => { // 每个用例前清空mock调用记录,避免用例间干扰 jest.clearAllMocks(); const module: TestingModule = await Test.createTestingModule({ controllers: [AccountsController], providers: [ AccountsService, { provide: getModelToken(Account.name), useValue: accountModel, }, ], }).compile(); controller = module.get<AccountsController>(AccountsController); service = module.get<AccountsService>(AccountsService); }); it('should be defined', () => { expect(controller).toBeDefined(); }); describe('create account POST /', () => { it('should call the right service', async () => { const method = jest.spyOn(service, 'createAccount').mockReturnValue(Promise.resolve(fakeAccount)); await controller.createAccount(postBody); // 注意断言参数要和业务实际传参一致,原测试里写的'hello',1是错误参数 expect(newrelic.recordCustomEvent).toHaveBeenCalledWith('accountCreated', expect.objectContaining({ accountId: expect.anything(), groupName: 'Palmeiras' })); expect(newrelic.recordMetric).toHaveBeenCalledWith('accounts_created', 1); expect(method).toHaveBeenCalledWith(postBody.name); }); }); });
方案2:全局Mock(推荐,一劳永逸适配所有测试文件)
在项目根目录新建__mocks__文件夹,在文件夹内新建newrelic.ts写入通用Mock逻辑:
// __mocks__/newrelic.ts export default { recordMetric: jest.fn(), recordCustomEvent: jest.fn(), noticeError: jest.fn(), startSegment: jest.fn(), startWebTransaction: jest.fn(), endTransaction: jest.fn(), addCustomAttribute: jest.fn(), // 按需补全项目用到的其他NewRelic方法 };
之后在Jest配置文件(jest.config.js或package.json的jest字段)对应的setupFilesAfterEnv指向的setup文件里,加一行全局注册:
// jest.setup.ts jest.mock('newrelic');
这样所有测试文件运行前都会自动把NewRelic替换为Mock版本,完全不会触发真实模块初始化,从根源上避免配置找不到的报错,也不用每个测试文件重复写Mock逻辑。
方案3:测试环境禁用NewRelic(不想写Mock时可选)
如果不想Mock模块,可以在测试环境的环境变量里添加以下配置,让NewRelic自动跳过初始化和校验:
NEW_RELIC_ENABLED=false NEW_RELIC_APP_NAME=test-service NEW_RELIC_LICENSE_KEY=test
这种方式会加载真实的NewRelic模块,测试启动速度会比Mock方案慢一点,适合需要验证NewRelic调用链路的场景。
补充提示:NewRelic默认要求配置文件放在项目根目录、命名为
newrelic.js,你现在把配置放在/src/newrelic.ts属于非标准路径,后续构建、部署时也容易出现配置丢失的问题,建议移到根目录改成.js格式,所有环境都能自动识别配置。
内容的提问来源于stack exchange,提问作者Luis Novo
相关产品推荐
相关产品推荐

