1

My implementation is based on this article: https://dev.to/nestjs/advanced-nestjs-how-to-build-completely-dynamic-nestjs-modules-1370

I want to test my generic, Twilio-based SMS sender service that I share between multiple parts of my application. I want to configure it when I'm importing it from somewhere else, so I'm writing it as a dynamic module. On top of that, the options that I pass to the dynamic module are themselves constructed dynamically, they are read from my .env file. I'm using the factory pattern when I'm registering my provider:

// app.module.ts

@Module({
  imports: [
    ConfigModule.forRoot({
      isGlobal: true,
      envFilePath: [
        '.env',
      ],
      validationSchema,
    }),  
    SharedSmsModule.registerAsync({
      imports: [ConfigModule],
      inject: [ConfigService],
      useFactory: (configService: ConfigService<EnvironmentVariables>) => {
        return {
          accountSid: configService.get('TWILIO_ACCOUNT_SID'),
          authToken: configService.get('TWILIO_AUTH_TOKEN'),
          smsSenderPhoneNumber: configService.get(
            'TWILIO_SMS_SENDER_PHONE_NUMBER'
          ),
        };
      },
    }),
  ],
})
export class AppModule {}

My shared-sms module calls the function provided in the registerAsync method in app.module.ts:

// shared-sms.module.ts

export interface SharedSmsModuleOptions {
  accountSid: string;
  authToken: string;
  smsSenderPhoneNumber: string;
}

export interface SharedSmsModuleAsyncOptions extends ModuleMetadata {
  imports: any[];
  inject: any[];
  useFactory?: (
    ...args: any[]
  ) => Promise<SharedSmsModuleOptions> | SharedSmsModuleOptions;
}

@Module({})
export class SharedSmsModule {
  static registerAsync(
    sharedSmsModuleAsyncOptions: SharedSmsModuleAsyncOptions
  ): DynamicModule {
    return {
      global: true,
      module: SharedSmsModule,
      imports: sharedSmsModuleAsyncOptions.imports,
      providers: [
        {
          provide: 'SHARED_SMS_OPTIONS',
          useFactory: sharedSmsModuleAsyncOptions.useFactory,
          inject: sharedSmsModuleAsyncOptions.inject || [],
        },
        SharedSmsService,
      ],
      exports: [SharedSmsService],
    };
  }
}

Now I have access to the options variables in my shared-sms.service:

// shared-sms.service

@Injectable()
export class SharedSmsService {
  private twilioClient: Twilio;

  constructor(
    @Inject('SHARED_SMS_OPTIONS') private options: SharedSmsModuleOptions
  ) {
    this.twilioClient = new Twilio(
      this.options.accountSid,
      this.options.authToken
    );
  }

  async sendSms(sendSmsDto: SendSmsDto): Promise<MessageInstance> {
    await validateOrReject(plainToInstance(SendSmsDto, sendSmsDto));

    const smsData = {
      from: this.options.smsSenderPhoneNumber,
      to: sendSmsDto.to,
      body: sendSmsDto.body,
    };

    return await this.twilioClient.messages.create(smsData);
  }
}

So long everything seems to be working. But I'm having issues when I'm trying to test the service's sendSms function. I can write tests that work when I'm providing hardcoded Twilio test account values in my test file. But I don't want to commit them to the repository, so I would want to get them from my .env file. I have tried providing everything to the Test.createTestingModule function when I'm creating my moduleRef, based on what I did in the code that I already wrote, but I couldn't specify the Twilio test account values dynamically. As I don't see documentation regarding this issue, I feel like that I'm either missing a conceptual point (providing so many things in the test seems like an overkill) or there is a trivial work-around. Please help me figure out how to pass those values to my tests from my .env file

szeb
  • 326
  • 3
  • 20
  • 1
    Do any of [the answers here help](https://stackoverflow.com/questions/55673424/nestjs-unable-to-read-env-variables-in-module-files-but-able-in-service-files)? Also, for testing, I would recommend mocking the Twilio service as you don't want to be making external API calls within your tests. – philnash Apr 08 '22 at 01:35

0 Answers0