Reputation: 1368
I am working on unit testing an Angular service that looks like so:
/* data.service.ts */
import { Injectable } from '@angular/core';
import { FooService } from './foo.service';
import { Data } from './data.model';
@Injectable({
providedIn: 'root'
})
export class DataService {
constructor(private fooService: FooService) {}
addData(data: Data) {
return this.fooService.getActiveFoo().addData(data);
}
}
FooService
is a service in the same module. It injects some values using InjectionTokens.
/* foo.service.ts */
import { Inject, Injectable } from '@angular/core';
import { KEY1, KEY2 } from './tokens';
import { Foo } from './foo.ts';
@Injectable({
providedIn: 'root'
})
export class FooService{
activeFoo: Foo;
constructor(@Inject(KEY1) private key1: string, @Inject(KEY2) private key2: string) {}
...
getActiveFoo() {
return this.activeFoo;
}
}
/* tokens.ts */
import { InjectionToken } from '@angular/core';
export const KEY1 = new InjectionToken('KEY_1');
export const KEY2 = new InjectionToken('KEY_2');
All three files are part of the same module, which is set up as so:
/* bar.module.ts */
import { ModuleWithProviders, NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { KEY1, KEY2 } from './tokens';
import { DataService } from './data.service';
import { FooService } from './foo.service';
@NgModule({
imports: [CommonModule],
})
export class BarModule {
static forRoot(key1Val: string, key2Val: string): ModuleWithProviders {
return {
ngModule: BarModule,
providers: [
DataService,
FooService,
{ provide: KEY1, useValue: key1Val},
{ provide: KEY2, useValue: key2Val}
]
};
}
}
This is working fine in the app itself. Now I am working on writing a unit test for data.service.ts
and am running into issues with the provided InjectionTokens.
/* data.service.spec.ts */
import { TestBed, inject } from '@angular/core/testing';
import { DataService } from './data.service';
import { FooService } from './foo.service';
import { KEY1, KEY2 } from './tokens';
describe('DataService', () => {
const mockFooService = jasmine.createSpyObj(['getActiveFoo']);
beforeEach(() => {
TestBed.configureTestingModule({
providers: [
DataService,
{ provide: FooService, useValue: mockFooService },
{ provide: KEY1, useValue: 'abc' },
{ provide: KEY2, useValue: '123' }
]
});
});
it('should be created', inject([DataService], (service: DataService) => {
expect(service).toBeTruthy();
}));
});
When I run my tests I am receiving an error saying
Error: StaticInjectorError(DynamicTestModule)[FooService -> InjectionToken KEY_1]:
StaticInjectorError(Platform: core)[FooService -> InjectionToken KEY_1]:
NullInjectorError: No provider for InjectionToken KEY_1!
I have tried using empty objects instead of strings in the test providers but still get the same error. Is there another way I need to be providing these tokens?
Upvotes: 1
Views: 2643
Reputation: 1368
I solved this by adding factory functions to the InjectionTokens that returns a default value for the token. When this is done it defaults to providing the tokens at the root injector.
Since both services were being provided by the root injector and the tokens were being provided in the BarModule
's injector they did not have visibility to each other in the test bed, but did when the entire module was loaded by my app since they were all present in the module. I confirmed this by removing providedIn
from the two services and leaving the tokens as they were originally.
My tokens now look like
/* tokens.ts */
import { InjectionToken } from '@angular/core';
export const KEY1 = new InjectionToken<string>('KEY_1', {factory: () => '' });
export const KEY2 = new InjectionToken<string>('KEY_2', {factory: () => '' });
As an added benefit, the tokens are now tree-shakeable. See here for more information.
Upvotes: 1