I have a component that I am trying to setup and test using TestBed.
This component contains one class that has a parameter in the constructor that is an interface, not a concrete class. This interface is satisfied by whatever class I choose to use (either the real one, or a mok one for unit testing). But when I am constructing the component that uses this service in TestBed, I cannot figure out how to define that parameter to the TestBed configuration.
Here is the TestBed config for the component:
describe('PanelContentAreaComponent', () => {
let component: PanelContentAreaComponent;
let fixture: ComponentFixture<PanelContentAreaComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ PanelContentAreaComponent
],
providers:[
MenuCommandService, ProcedureDataService, IOpenService],
schemas: [CUSTOM_ELEMENTS_SCHEMA]
})
.compileComponents();
}));
The service which is having trouble being constructed in TestBed is ProcedureDataService . It's definition is below:
@Injectable()
export class ProcedureDataService {
serverOpenFile: OpenFile;
constructor(private _openService: IOpenService) {
this.serverOpenFile = emptyFileStatus;
}
The one parameter in the constructor of the ProcedureDataService
is IOpenService
whose definition is :
export interface IOpenService {
openFile(fileType: string, dataType: string, filePath: string) ;
}
As you can see this is an interface, not a concrete class.
In my service unit test, we mock the IOpenService by implementing it as follows:
export class mockOpenService implements IOpenService{
constructor(){}
openFile(fileType: string, dataType: string, filePath: string) {
let fileContent: OpenFile;
...
...
[fake the data with mok junk]
...
fileContent = {
'filePath': filePath,
'fileName': name,
'openSuccess': isSuccess,
'error': errorMsg,
'fileData': jsonData
};
return Observable.of(fileContent);
}
}
This works great in the ProcedureDataService service unit test. And, of course, in the real code, we implement the IOpenService with the full implemented file open service that gets the data properly.
But in trying to use this service inside of a component I get the following error:
PanelContentAreaComponent should create FAILED
Failed: IOpenService is not defined
ReferenceError: IOpenService is not defined
This makes sense, so then I am trying to figure out how to tell TestBed that I have a concrete class implementation of this IOpenService which I wish to use. I tried this, but it fails:
describe('PanelContentAreaComponent', () => {
let component: PanelContentAreaComponent;
let fixture: ComponentFixture<PanelContentAreaComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ PanelContentAreaComponent
],
providers:[
{provide: IOpenService, useClass: mockOpenService},
MenuCommandService, ProcedureDataService, IOpenService],
schemas: [CUSTOM_ELEMENTS_SCHEMA]
})
.compileComponents();
}));
The compiler is telling me:
(31,19): error TS2693: 'IOpenService' only refers to a type, but is being used as a value here.
and I am still getting :
PanelContentAreaComponent should create FAILED
Failed: IOpenService is not defined
ReferenceError: IOpenService is not defined
So how do I instruct TestBed
that I have a specific class (mockOpenService
) implementation of an interface parameter (IOpenService
) needed for the service (ProcedureDataService
) being provided to test this component (PanelContentAreaComponent
)?
Interfaces can't be used as token. This is explained in the Angular docs DI chapter Dependency injection tokens
TypeScript interfaces aren't valid tokens
export interface AppConfig { apiEndpoint: string; title: string; } export const HERO_DI_CONFIG: AppConfig = { apiEndpoint: 'api.heroes.com', title: 'Dependency Injection' };
The
HERO_DI_CONFIG
constant has an interface,AppConfig
. Unfortunately, we cannot use a TypeScript interface as a token:// FAIL! Can't use interface as provider token [{ provide: AppConfig, useValue: HERO_DI_CONFIG })] // FAIL! Can't inject using the interface as the parameter type constructor(private config: AppConfig){ }
That seems strange if we're used to dependency injection in strongly typed languages, where an interface is the preferred dependency lookup key.
It's not Angular's fault. An interface is a TypeScript design-time artifact. JavaScript doesn't have interfaces. The TypeScript interface disappears from the generated JavaScript. There is no interface type information left for Angular to find at runtime.
The docs goes on to explain that you should create an OpaqueToken
.
import { OpaqueToken } from '@angular/core'; export let APP_CONFIG = new OpaqueToken('app.config'); providers: [{ provide: APP_CONFIG, useValue: HERO_DI_CONFIG }] constructor(@Inject(APP_CONFIG) config: AppConfig) { this.title = config.title; }
It's ok for this example, but in our case of the service, this is not the most elegant solution. Personally, I think the more elegant solution is to not use interfaces at all for services. Instead use abstract classes. Abstract classes are transpiled to real code, just as a normal class would be. So you can use it as a token
export abstract class IOpenService {
abstract openFile(fileType: string, dataType: string, filePath: string): any ;
}
class OpenService extends IOpenService {
openFile(fileType: string, dataType: string, filePath: string): any {
}
}
Now you can do
{ provide: IOpenService, useClass: OpenService }
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With