Использование Angular2 TestBed для проверки сервиса с неконкретным параметром интерфейса класса
У меня есть компонент, который я пытаюсь настроить и протестировать с помощью TestBed.
Этот компонент содержит один класс, имеющий параметр в конструкторе, который является интерфейсом, а не конкретным классом. Этот интерфейс удовлетворяется любым классом, который я выберу (реальный или mok для модульного тестирования). Но когда я создаю компонент, который использует этот сервис в TestBed, я не могу понять, как определить этот параметр для конфигурации TestBed.
Вот конфигурация TestBed для компонента:
describe('PanelContentAreaComponent', () => {
let component: PanelContentAreaComponent;
let fixture: ComponentFixture<PanelContentAreaComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ PanelContentAreaComponent
],
providers:[
MenuCommandService, ProcedureDataService, IOpenService],
schemas: [CUSTOM_ELEMENTS_SCHEMA]
})
.compileComponents();
}));
Служба, которая испытывает затруднения при построении в TestBed, представляет собой StructureDataService. Это определение ниже:
@Injectable()
export class ProcedureDataService {
serverOpenFile: OpenFile;
constructor(private _openService: IOpenService) {
this.serverOpenFile = emptyFileStatus;
}
Один параметр в конструкторе ProcedureDataService
является IOpenService
чье определение:
export interface IOpenService {
openFile(fileType: string, dataType: string, filePath: string) ;
}
Как видите, это интерфейс, а не конкретный класс.
В моем тесте модуля обслуживания мы смоделировали IOpenService, реализовав его следующим образом:
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);
}
}
Это прекрасно работает в тестовом модуле ServiceDataService. И, конечно же, в реальном коде мы реализуем IOpenService с полностью реализованной службой открытия файлов, которая правильно получает данные.
Но при попытке использовать этот сервис внутри компонента я получаю следующую ошибку:
PanelContentAreaComponent should create FAILED
Failed: IOpenService is not defined
ReferenceError: IOpenService is not defined
Это имеет смысл, поэтому я пытаюсь выяснить, как сказать TestBed, что у меня есть конкретная реализация класса этого IOpenService, которую я хочу использовать. Я пытался это, но это не удается:
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();
}));
Компилятор говорит мне:
(31,19): error TS2693: 'IOpenService' only refers to a type, but is being used as a value here.
и я все еще получаю:
PanelContentAreaComponent should create FAILED
Failed: IOpenService is not defined
ReferenceError: IOpenService is not defined
Итак, как мне инструктировать TestBed
что у меня есть определенный класс (mockOpenService
) реализация параметра интерфейса (IOpenService
) необходим для обслуживания (ProcedureDataService
) предоставляется для тестирования этого компонента (PanelContentAreaComponent
)?
1 ответ
Интерфейсы не могут быть использованы в качестве токена. Это объясняется в главе Angular docs DI токены зависимости
Интерфейсы TypeScript не являются допустимыми токенами
export interface AppConfig { apiEndpoint: string; title: string; } export const HERO_DI_CONFIG: AppConfig = { apiEndpoint: 'api.heroes.com', title: 'Dependency Injection' };
HERO_DI_CONFIG
константа имеет интерфейс,AppConfig
, К сожалению, мы не можем использовать интерфейс TypeScript в качестве токена:// 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){ }
Это кажется странным, если мы привыкли к внедрению зависимостей в строго типизированных языках, где интерфейс является предпочтительным ключом поиска зависимостей.
Это не ошибка Angular. Интерфейс - это артефакт времени разработки TypeScript. У JavaScript нет интерфейсов. Интерфейс TypeScript исчезает из сгенерированного JavaScript. Для Angular не осталось информации о типе интерфейса для поиска во время выполнения.
Документы продолжают объяснять, что вы должны создать 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; }
Это нормально для этого примера, но в нашем случае с сервисом это не самое элегантное решение. Лично я думаю, что более элегантное решение - вообще не использовать интерфейсы для сервисов. Вместо этого используйте абстрактные классы. Абстрактные классы переносятся в реальный код, как и обычный класс. Таким образом, вы можете использовать его в качестве токена
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 {
}
}
Теперь вы можете сделать
{ provide: IOpenService, useClass: OpenService }