Reputation: 2688
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
Reputation: 388
There are 2 ways you can do resolve this:
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();
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
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
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
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
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