如何在NestJS的Jest测试中Mock GRPC客户端?
问题描述
我有一段NestJS中调用gRPC服务的业务逻辑代码,以及对应的Jest测试代码,但运行测试时始终出现「connection not found」错误,请问如何修改测试文件,Mock其中的gRPC客户端来解决这个问题?
业务逻辑代码
import { Injectable, Logger, OnModuleInit } from '@nestjs/common'; import { Client, ClientGrpc } from '@nestjs/microservices'; import { catchError, map } from 'rxjs'; import { ICmdUserUsecase } from 'src/adaptor/usecase/ICmdUserUsecase'; import { dataServiceGrpcOptions } from 'src/app/data.options'; import { ICmdRepo } from 'src/adaptor/repo/ICmdRepo'; @Injectable() export class CmdUserUsecase implements ICmdUserUsecase, OnModuleInit { private dataCmdRepo: ICmdRepo; constructor(private readonly logger: Logger) {} @Client({ ...dataServiceGrpcOptions, }) dataServiceClientGrpc: ClientGrpc; onModuleInit() { this.dataCmdRepo = this.dataServiceClientGrpc.getService<ICmdRepo>('DataService'); } async createUserProfile(data: any) { return new Promise((resolve, reject) => { const userData = this.dataCmdRepo.createUser(data).pipe( catchError((err) => { if (err.code === 2) { err.message = 'Data Server is down'; throw new Error(err.message); } else { throw new Error(err.message); } }), map((results) => { return results; }), ); userData.subscribe( (rate) => { resolve(rate); }, (error) => { reject(error); }, ); }); } async createUser(data: any): Promise<any> { return await this.createUserProfile(data) .then((res) => { this.logger.log(res); return res; }) .catch((err) => { return err; }); } }
原测试代码
import { INestApplication, Logger } from '@nestjs/common'; import { ConfigModule } from '@nestjs/config'; import { ClientsModule } from '@nestjs/microservices'; import { Test, TestingModule } from '@nestjs/testing'; import { ICmdUserUsecase } from 'src/adaptor/usecase/ICmdUserUsecase'; import { IQueryUserUsecase } from 'src/adaptor/usecase/IQueryUserUsecase'; import { userServiceGrpcOptions } from 'src/app/data.options'; import { configuration } from 'src/utils/configuration'; import { Mock } from 'src/utils/Mocks/mock'; import { MetricsTelemetryModule } from 'telemetry-lib'; import { QueryUserUsecase } from '../query-user-usecase/query.user.usecase'; import { CmdUserUsecase } from './cmd.user.usecase'; describe('CMD user usecase ', () => { let app: INestApplication; let cmdUserUsecase: CmdUserUsecase; beforeEach(async () => { const moduleRef: TestingModule = await Test.createTestingModule({ imports: [ ConfigModule.forRoot({ load: [configuration], isGlobal: true, cache: true, }), MetricsTelemetryModule.forRoot({ apiMetrics: true, }), ClientsModule.register([ { name: 'USER_SERVICE', ...userServiceGrpcOptions, }, ]), ], providers: [ CmdUserUsecase, { provide: ICmdUserUsecase, useClass: CmdUserUsecase, }, { provide: IQueryUserUsecase, useClass: QueryUserUsecase, }, { provide: Logger, useValue: { log: jest.fn((data) => data), }, }, ], exports: [], }).compile(); app = moduleRef.createNestApplication(); await app.init(); cmdUserUsecase = moduleRef.get<CmdUserUsecase>(CmdUserUsecase); }); afterAll(async () => { await app.close(); }); describe('root', () => { it('CmdUserUsecase should be defined', () => { expect(cmdUserUsecase).toBeDefined(); }); it('CmdUserUsecase"s createUser should be called when invoked', async () => { try { await cmdUserUsecase.createUser(Mock.userMock); expect(cmdUserUsecase.createUser).toHaveBeenCalled(); } catch (error) { expect(error).toEqual(error); } }); it('CmdUserUsecase"s createUser should RETURN suceess message', async () => { try { const result = await cmdUserUsecase.createUser(Mock.userMock); expect(result.status).toBe('success'); } catch (error) { expect(error).toEqual(error); } }); it('CmdUserUsecase"s createUserProfile should be called when invoked', async () => { try { await cmdUserUsecase.createUserProfile(Mock.userMock); expect(cmdUserUsecase.createUserProfile).toHaveBeenCalled(); } catch (error) { expect(error).toEqual(error); } }); it('CmdUserUsecase"s createUserProfile should RETURN suceess message', async () => { try { const result = await cmdUserUsecase.createUserProfile(Mock.userMock); expect(result).toBe('success'); } catch (error) { expect(error).toEqual(error); } }); }); });
解决方案
核心思路是Mock掉gRPC客户端的实际调用逻辑,避免测试时尝试建立真实的gRPC连接。具体修改如下:
修改后的完整测试代码
import { Logger } from '@nestjs/common'; import { ConfigModule } from '@nestjs/config'; import { ClientGrpc } from '@nestjs/microservices'; import { Test, TestingModule } from '@nestjs/testing'; import { of, throwError } from 'rxjs'; import { ICmdUserUsecase } from 'src/adaptor/usecase/ICmdUserUsecase'; import { ICmdRepo } from 'src/adaptor/repo/ICmdRepo'; import { configuration } from 'src/utils/configuration'; import { Mock } from 'src/utils/Mocks/mock'; import { MetricsTelemetryModule } from 'telemetry-lib'; import { CmdUserUsecase } from './cmd.user.usecase'; describe('CMD user usecase ', () => { let cmdUserUsecase: CmdUserUsecase; let mockCmdRepo: Partial<ICmdRepo>; beforeEach(async () => { // 初始化Mock的gRPC服务实现,模拟createUser方法 mockCmdRepo = { createUser: jest.fn(() => of({ status: 'success' })) }; // Mock ClientGrpc实例,让getService返回我们的mockCmdRepo const mockClientGrpc: Partial<ClientGrpc> = { getService: jest.fn(() => mockCmdRepo) }; const moduleRef: TestingModule = await Test.createTestingModule({ imports: [ ConfigModule.forRoot({ load: [configuration], isGlobal: true, cache: true, }), MetricsTelemetryModule.forRoot({ apiMetrics: true, }), // 移除真实gRPC客户端注册,避免建立实际连接 ], providers: [ CmdUserUsecase, { provide: ICmdUserUsecase, useClass: CmdUserUsecase, }, { provide: Logger, useValue: { log: jest.fn((data) => data), }, }, ], }).compile(); cmdUserUsecase = moduleRef.get<CmdUserUsecase>(CmdUserUsecase); // 替换业务代码中的真实gRPC客户端为Mock实例 cmdUserUsecase.dataServiceClientGrpc = mockClientGrpc as ClientGrpc; // 手动触发初始化逻辑,确保dataCmdRepo指向Mock实例 cmdUserUsecase.onModuleInit(); }); describe('核心方法测试', () => { it('CmdUserUsecase 实例应被正确定义', () => { expect(cmdUserUsecase).toBeDefined(); }); it('createUser 调用后应返回成功状态', async () => { const result = await cmdUserUsecase.createUser(Mock.userMock); expect(result.status).toBe('success'); // 验证gRPC方法被正确调用 expect(mockCmdRepo.createUser).toHaveBeenCalledWith(Mock.userMock); // 验证Logger日志方法被触发 expect(cmdUserUsecase['logger'].log).toHaveBeenCalled(); }); it('createUserProfile 调用后应返回成功结果', async () => { const result = await cmdUserUsecase.createUserProfile(Mock.userMock); expect(result).toEqual({ status: 'success' }); expect(mockCmdRepo.createUser).toHaveBeenCalledWith(Mock.userMock); }); it('gRPC服务返回错误时,createUserProfile应抛出对应提示', async () => { // 修改Mock行为,模拟gRPC服务错误 mockCmdRepo.createUser = jest.fn(() => throwError(() => ({ code: 2, message: 'Original error' }))); cmdUserUsecase.onModuleInit(); await expect(cmdUserUsecase.createUserProfile(Mock.userMock)).rejects.toThrow('Data Server is down'); }); }); });
关键修改说明
- 移除真实gRPC客户端注册:删掉原测试中
ClientsModule.register部分,避免测试时尝试建立真实连接。 - Mock gRPC服务实现:创建
mockCmdRepo模拟gRPC服务的createUser方法,返回预期的Observable结果(成功/错误)。 - 替换业务代码中的gRPC客户端:手动给
cmdUserUsecase.dataServiceClientGrpc赋值Mock的ClientGrpc实例,替代真实客户端。 - 手动触发初始化逻辑:调用
onModuleInit确保业务代码中的dataCmdRepo指向Mock实例。 - 优化测试用例:新增错误场景测试,同时验证Mock方法的调用情况,提升测试覆盖率。
内容的提问来源于stack exchange,提问作者Ritesh Kumar
相关产品推荐
相关产品推荐

