Использование 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 }
Другие вопросы по тегам