使用 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 }
我有一个组件正在尝试使用 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 }