Jon Lauridsen
Jon Lauridsen

Reputation: 2688

How to test a nestjs service by passing in a ConfigService with custom values?

I've created a service, and the module for it looks like this:

launchdarkly.module.ts

@Module({
  providers: [LaunchdarklyService],
  exports: [LaunchdarklyService],
  imports: [ConfigService],
})
export class LaunchdarklyModule {}

(this service/module is to let the application use LaunchDarkly feature-flagging)

I'm happy to show the service-implementation if you'd like, but to keep this question shorter I skipped it. The important point is that this service imports the ConfigService (which it uses to grab the LaunchDarkly SDK key).

But how can I test the Launchdarkly service? It reads a key from ConfigService so I want to write tests where ConfigService has various values, but after hours of trying I can't figure out how to configure ConfigService in a test.

Here's the test:

launchdarkly.service.spec.ts

describe('LaunchdarklyService', () => {
  let service: LaunchdarklyService;

  beforeEach(async () => {
    const module: TestingModule = await Test.createTestingModule({
      providers: [LaunchdarklyService],
      imports: [ConfigModule],
    }).compile();

    service = module.get<LaunchdarklyService>(LaunchdarklyService);
  });

  it("should not create a client if there's no key", async () => {
    // somehow I need ConfigService to have key FOO=undefined for this test
    expect(service.client).toBeUndefined();
  });

  it("should create a client if an SDK key is specified", async () => {
    // For this test ConfigService needs to specify FOO=123
    expect(service.client).toBeDefined();
  });
})

I'm open for any non-hacky suggestions, I just want to feature-flag my application!

Upvotes: 24

Views: 32475

Answers (5)

Debojyoti Chatterjee
Debojyoti Chatterjee

Reputation: 388

There are 2 ways you can do resolve this:

  1. When you're creating a testing module "import" the config module the same way it was imported in your application root module: For Example:
const module = await Test.createTestingModule({
  imports: [
    CacheModule.register({ isGlobal: true, ttl: 60000 }),
    ConfigModule.forRoot({
      load: Configs, // imported from another file
      ignoreEnvFile: false,
      isGlobal: true,
      cache: true,
      envFilePath: [".env"],
    }),
  ],
  providers: [ConfigService],
})
  .overrideProvider(PinoLogger)
  .useValue({
    info: jest.fn(() => Promise.resolve(null)),
  })
  .compile();

  1. Mock the config service if you do not have a huge list of ENV Variables
const module = await Test.createTestingModule({
  imports: [CacheModule.register({ isGlobal: true, ttl: 60000 })],
  providers: [
    {
      provide: configService,
      useValue: {
        get: jest.fn(() =>
          Promise.resolve({
            VARIABLE_NAME: VALUE,
          })
        ),
      },
    },
  ],
})
  .overrideProvider(PinoLogger)
  .useValue({
    info: jest.fn(() => Promise.resolve(null)),
  })
  .compile();

Upvotes: 1

Matheus Landuci
Matheus Landuci

Reputation: 171

You can get a cleaner syntax using the forFeature method provided by ConfigModule.

It accepts an async function that needs to return an object (in your case, your Env object).

The advantage of using the forFeature method is that you can register a partial object so you won't need to worry about other variables (or doing a lot of ifs).

beforeEach(async () => {
  const moduleRef = await Test.createTestingModule({
    imports: [
      ConfigModule.forFeature(async () => ({
        ANY_KEY_YOU_WANT: 'Any_Value'
      }))
    ],
    providers: [AnyService]
  }).compile()

  service = moduleRef.get<AnyService>(AnyService)
})

Upvotes: 12

buddha
buddha

Reputation: 161

I've done this by overwriting the ConfigService provider as follows:

import { CONFIGURATION_TOKEN } from '@nestjs/config/dist/config.constants';
import { Inject, Injectable, Optional } from '@nestjs/common';

@Injectable()
export class TestConfigService {
  private initialConfig: Record<string, any>;
  constructor(
    @Optional()
    @Inject(CONFIGURATION_TOKEN)
    private internalConfig: Record<string, any> = {},
  ) {
    this.initialConfig = internalConfig;
  }

  get(key: string, def: any) {
    const result = findByPath(this.internalConfig, key);
    return result || def;
  }

  set(key: string, val: any) {
    assignRawDataTo(this.internalConfig, { [key]: val });
  }

  reset() {
    this.internalConfig = this.initialConfig;
  }
}

The findByPath and assignRawDataTo are some utility functions to get and set data to the internalConfig.

Now you can setup your testing module as below:

return Test.createTestingModule({
    imports: [
        ConfigModule.forRoot({
            load: [
                 () => import('./test.config').then((module) => module.default),
             ],
             isGlobal: true,
         }),
      ],
}).overrideProvider(ConfigService)
    .useClass(TestConfigService);

Upvotes: 0

Moeid Heidari
Moeid Heidari

Reputation: 49

Instead of providing ConfigService you need to import the ConfigModule with mocked data. As an example

imports: [CommonModule,ConfigModule.forRoot({
                ignoreEnvVars: true,
                ignoreEnvFile: true,
                load: [() => ({ IntersectionOptions: { number_of_decimal_places: '3' }})],
            })],

Upvotes: 5

Jay McDoniel
Jay McDoniel

Reputation: 70550

Assuming the LaunchdarklyService needs the ConfigService and that is injected into the constructor, you can provide a mock variation of the ConfigService by using a Custom Provider to give back the custom credentials you need. For example, a mock for your test could look like

describe('LaunchdarklyService', () => {
  let service: LaunchdarklyService;
  let config: ConfigService;

  beforeEach(async () => {
    const module: TestingModule = await Test.createTestingModule({
      providers: [LaunchdarklyService, {
        provide: ConfigService,
        useValue: {
          get: jest.fn((key: string) => {
            // this is being super extra, in the case that you need multiple keys with the `get` method
            if (key === 'FOO') {
              return 123;
            }
            return null;
          })
        }
      ],
    }).compile();

    service = module.get<LaunchdarklyService>(LaunchdarklyService);
    config = module.get<ConfigService>(ConfigService);
  });

  it("should not create a client if there's no key", async () => {
    // somehow I need ConfigService to have key FOO=undefined for this test
    // we can use jest spies to change the return value of a method
    jest.spyOn(config, 'get').mockReturnedValueOnce(undefined);
    expect(service.client).toBeUndefined();
  });

  it("should create a client if an SDK key is specified", async () => {
    // For this test ConfigService needs to specify FOO=123
    // the pre-configured mock takes care of this case
    expect(service.client).toBeDefined();
  });
})

Upvotes: 50

Related Questions