nestjsgrpcgrpc-nodenestjs-confignestjs-testing

NestJS - gRPC Client E2E Test


I am trying to write an e2e test for my NestJS microservice that has a gRPC client. No matter what I do to try and mock the ClientsModule in the e2e test it still seems to not pick up the config and is unable to locate the *.proto file.

Please find a sample of my code below:

The clients module in the app.module.ts

// src/app.module.ts
@Module({
 imports: [
   HttpModule,
   ...
   ...
   ClientsModule.register([
     {
       name: 'USERS_PACKAGE',
       transport: Transport.GRPC,
       options: {
         package: 'users',
         credentials: credentials.createInsecure(),
         protoPath: join(__dirname, '../proto/users.proto'),
         url: 'users-grpc-server.users:50051',
      },
    },
  ]),
 ],
controllers: [UsersController],
providers: [UsersService, UsersClient],
})
export class AppModule {}

The users client to make the gRPC call to the gRPC Server NestJS microservice:

// src/clients/users.client.ts

@Injectable()
export class UsersClient implements OnModuleInit {
  private readonly logger: Logger = new Logger(UsersClient.name);
  private readonly API_KEY: string = this.configService.get<string>('services.v1.api-key');

  private usersController: usersController;

  constructor(@Inject('USERS_PACKAGE') private client: ClientGrpc, private configService: ConfigService) {}

  onModuleInit() {
    this.usersController = this.client.getService<UsersController>('UsersController');
  }

  async getUsers(headers: IncomingHttpHeaders, userId: string): Promise<Users> {
  
    let users: Users = null;

    const metadata = new Metadata();
    metadata.add('Authorization', headers.authorization);
    metadata.add('x-api-key', this.API_KEY);

    try {
      users = await this.usersController.getUsers({}, metadata);
    } catch (e) {
      const { message, response: { status = 500 } = {}, stack } = e;
      this.logger.error(stack);
      throw new HttpException(message, status);
    }

    return users;
  }
}

The update nest-cli.json

{
  "collection": "@nestjs/schematics",
  "sourceRoot": "src",
  "compilerOptions": {
    "assets": [{"include": "../config/*.yaml", "outDir": "./dist/config"}, {"include": "**/*.proto", "outDir": "./dist"}]
  }
}

The e2e test configuration

 // test/app.e2e-spec.json

  let app: NestFastifyApplication;
  let httpService: HttpService;
  let cacheManager: Cache;

  beforeAll(async () => {
    const module: TestingModule = await Test.createTestingModule({
      imports: [
        HttpModule,
        ...
        ...
        ClientsModule,
      ],
    })
      .overrideProvider(ClientsModule)
      .useValue({
        name: 'USERS_PACKAGE',
        transport: Transport.GRPC,
        options: {
          package: 'users',
          credentials: ServerCredentials.createInsecure(),
          protoPath: join(__dirname, '../src/proto/users.proto'),
          url: 'localhost:50051',
        },
      })
      .compile();

    app = module.createNestApplication(new FastifyAdapter());

    await app.init();
    await app.getHttpAdapter().getInstance().ready();

    httpService = module.get<HttpService>(HttpService);
    cacheManager = module.get<Cache>(CACHE_MANAGER);
});

The error:

  ● Test suite failed to run

    The invalid .proto definition (file at "/users-service-grpc-client/app/proto/users.proto" not found)

    > 26 |     ClientsModule.register([
         |                   ^
      27 |       {
      28 |         name: 'USERS_PACKAGE',
      29 |         transport: Transport.GRPC,

      at ClientGrpcProxy.loadProto (node_modules/@nestjs/microservices/client/client-grpc.js:220:39)
      at ClientGrpcProxy.createClients (node_modules/@nestjs/microservices/client/client-grpc.js:193:34)
      at new ClientGrpcProxy (node_modules/@nestjs/microservices/client/client-grpc.js:29:33)
      at Function.create (node_modules/@nestjs/microservices/client/client-proxy-factory.js:27:24)
      at node_modules/@nestjs/microservices/module/clients.module.js:12:80
          at Array.map (<anonymous>)
      at Function.register (node_modules/@nestjs/microservices/module/clients.module.js:10:41)
      at Object.<anonymous> (src/app.module.ts:26:19)

It seems like the the e2e test isnt using the ClientsModule configuration from the test and is still using the ClientsModule configuration from the app.module.ts.

Does anyone know of a way to configure this correctly ?


Solution

  • I am not sure how this is working but I changed the ClientsModule in the src/app.module.ts to be registerAsync because I wanted to make the url dynamic and it now seems to be working correctly

    // src/app.module.ts
    @Module({
     imports: [
       HttpModule,
       ...
       ...
       ClientsModule.registerAsync([
        {
          name: 'USERS_PACKAGE',
          imports: [ConfigModule],
          useFactory: async (configService: ConfigService) => ({
            transport: Transport.GRPC,
            options: {
              package: 'users',
              credentials: credentials.createInsecure(),
              protoPath: join(__dirname, '../proto/users.proto'),
              url: configService.get<string>('services.users-grpc-server-service.url'),
            },
          }),
          inject: [ConfigService],
        },
      ]),
     ],
    controllers: [UsersController],
    providers: [UsersService, UsersClient],
    })
    export class AppModule {}

    Note: In the E2E test, I also had to mock the grpc call using overrideProvider

    // test/app.e2e-spec.ts
    
    const module: TestingModule = await Test.createTestingModule({
          imports: [
            HttpModule,
            ...
            ...
            ClientsModule,
          ],
        })
          .overrideProvider('USERS_PACKAGE')
          .useValue({
            getService: () => ({
              getUsers: jest.fn().mockReturnValue({
                "name": "test user",
                "age": "55",
                "height": "1.89"
              }),
            }),
          })
          .compile();