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

如何在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');
    });
  });
});

关键修改说明

  1. 移除真实gRPC客户端注册:删掉原测试中ClientsModule.register部分,避免测试时尝试建立真实连接。
  2. Mock gRPC服务实现:创建mockCmdRepo模拟gRPC服务的createUser方法,返回预期的Observable结果(成功/错误)。
  3. 替换业务代码中的gRPC客户端:手动给cmdUserUsecase.dataServiceClientGrpc赋值Mock的ClientGrpc实例,替代真实客户端。
  4. 手动触发初始化逻辑:调用onModuleInit确保业务代码中的dataCmdRepo指向Mock实例。
  5. 优化测试用例:新增错误场景测试,同时验证Mock方法的调用情况,提升测试覆盖率。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.18 22:40:35