NestJS - gRPC Client E2E Test

1k Views Asked by At

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 ?

2

There are 2 best solutions below

1
On BEST ANSWER

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();

0
On

In my case I had to do

import { join } from 'path';
import { Env, NODE_ENV } from '@backend/utilities';

export const AUTH_SERVICE_PROTO_PATH =
  NODE_ENV === Env.E2E
    ? 'apps/backend/users/src/proto/auth.proto'
    : join(__dirname, 'proto/auth.proto');

so for e2e testing we use the static path.