使用 Angular2 TestBed 模拟具有非具体 class 接口参数的服务

Using Angular2 TestBed to Mock a service with a non-concrete class interface parameter

我有一个组件正在尝试使用 TestBed 进行设置和测试。

此组件包含一个 class,它在构造函数中有一个参数,该参数是一个接口,而不是具体的 class。我选择使用的任何 class 都可以满足此接口(真实接口或用于单元测试的 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 中构建时遇到问题的服务是 ProcedureDataService 。它的定义如下:

@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) ;
}

如您所见,这是一个接口,而不是具体的 class。

在我的服务单元测试中,我们通过如下实现来模拟 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);

    }

}

这在 ProcedureDataService 服务单元测试中效果很好。当然,在实际代码中,我们使用完整实现的文件打开服务来实现 IOpenService,以便正确获取数据。

但是在尝试在组件内部使用此服务时出现以下错误:

PanelContentAreaComponent should create FAILED
        Failed: IOpenService is not defined
        ReferenceError: IOpenService is not defined

这是有道理的,所以我想弄清楚如何告诉 TestBed 我有一个我希望使用的 IOpenService 的具体 class 实现。我试过了,但失败了:

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 我有一个特定的 class (mockOpenService) 实现服务所需的接口参数 (IOpenService) (ProcedureDataService) 被提供来测试这个组件 (PanelContentAreaComponent)?

接口不能用作令牌。这在 Angular 文档 DI 章节 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.

文档继续说明您应该创建一个 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;
}

这个例子没问题,但在我们的服务案例中,这不是最优雅的解决方案。就个人而言,我认为更优雅的解决方案是根本不为服务使用接口。而是使用抽象 classes。抽象 classes 被转换为实际代码,就像正常的 class 一样。所以你可以将它用作令牌

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 }