[英]Issue testing an Angular service with abstract class dependencies
I have a service, which requires a dependency to another service, which in turns requires dependencies on two abstract classes.我有一个服务,它需要依赖另一个服务,而另一个服务又需要依赖两个抽象类。
(ThemeConfigService -> (SettingsService -> SettingsLoader, NavigationLoader))
I have gotten so far as to have the test fail because it cannot find the methods exposed via the abstract classes (not a function exception).我已经让测试失败了,因为它找不到通过抽象类公开的方法(不是 function 异常)。
I'm not sure how I would get passed this, various searches online have not proved very helpful.我不确定我将如何通过这个,网上的各种搜索并没有证明很有帮助。
Here is the theme configuration service for which I'm trying to flesh out a test " theme-config.service.ts
"这是我试图充实测试“
theme-config.service.ts
”的主题配置服务
@Injectable({
providedIn: 'root'
})
export class ThemeConfigService {
constructor(
private platform: Platform,
private router: Router,
private settings: SettingsService
) {
// code removed for brevity
}
}
Here is the service being tested " settings.service.ts
"这是正在测试的服务“
settings.service.ts
”
@Injectable()
export class SettingsService {
constructor(public settingsLoader: SettingsLoader,
public navigationLoader: NavigationLoader) { }
public settings(): Observable<any> {
return this.settingsLoader.retrieveSettings();
}
public navigation(): Observable<any> {
return this.navigationLoader.retrieveNavigation();
}
}
Here is the SettingsLoader
class, the NavigationLoader
looks exactly the same.这是
SettingsLoader
class, NavigationLoader
看起来完全一样。 They have to be separate classes from a design perspective:从设计的角度来看,它们必须是单独的类:
export abstract class SettingsLoader {
abstract retrieveSettings(): Observable<any>;
}
My unit test looks something like this:我的单元测试看起来像这样:
describe('ThemeConfigService', () => {
let service: ThemeConfigService;
let router: Router;
beforeEach(() => {
TestBed.configureTestingModule({
imports: [
RouterTestingModule.withRoutes([])
],
providers: [
Platform,
SettingsService,
SettingsLoader,
NavigationLoader
]
});
router = TestBed.inject(Router);
service = TestBed.inject(ThemeConfigService);
});
it('should be created', async(inject([Platform, Router, SettingsService, SettingsLoader, NavigationLoader],
(platform: Platform, router: Router, settings: SettingsService, settingsLoader: SettingsLoader, navigationLoader: NavigationLoader) => {
expect(service).toBeTruthy();
})));
});
The error returned by Karma is: Karma 返回的错误是:
TypeError: this.settingsLoader.retrieveSettings is not a function
which to me proves that it cannot resolve the abstract classes.
For that reasons have I gone ahead and created something like this:出于这个原因,我继续创建了这样的东西:
export class SettingsFakeLoader extends SettingsLoader {
retrieveSettings(): Observable<any> {
return of({});
}
}
And tried changing the injection of the SettingsLoader
and NavigationLoader
classes with these, then Karma responds with:并尝试使用这些更改
SettingsLoader
和NavigationLoader
类的注入,然后 Karma 响应:
NullInjectorError: R3InjectorError(DynamicTestModule)[ThemeConfigService -> SettingsService -> SettingsLoader -> SettingsLoader]:
NullInjectorError: No provider for SettingsLoader!
The amended beforeEach
for the theme-config.service.spec.ts
file:修改了
theme-config.service.spec.ts
文件的beforeEach
:
beforeEach(() => {
TestBed.configureTestingModule({
imports: [
RouterModule,
RouterTestingModule.withRoutes([])
],
providers: [
Platform,
SettingsService,
SettingsFakeLoader,
NavigationFakeLoader
]
});
router = TestBed.inject(Router);
service = TestBed.inject(ThemeConfigService);
});
Usually, I would not try and test something I perceive to be this complex.通常,我不会尝试测试我认为如此复杂的东西。 Maybe I'm just not seeing the 'solution'.
也许我只是没有看到“解决方案”。
Any help would be much appreciated, as I'll have a similar scenario to resolve later down the line of this application's development.任何帮助将不胜感激,因为稍后我将在此应用程序的开发过程中解决类似的情况。
I went the instantiation route, instead of relying on the dependency injection.我走的是实例化路线,而不是依赖依赖注入。 This might not be a viable solution, and would still like an answer on the original question from someone with a better approach.
这可能不是一个可行的解决方案,并且仍然希望有更好方法的人回答原始问题。
The updated describe
for the theme-config.service.spe.ts
file: theme-config.service.spe.ts
文件的更新describe
:
describe('ThemeConfigService', () => {
let service: ThemeConfigService;
let sLoader: SettingsLoader;
let nLoader: NavigationLoader;
let sService: SettingsService;
beforeEach(() => {
TestBed.configureTestingModule({
imports: [
RouterTestingModule.withRoutes([])
],
providers: [
ThemeConfigService,
SettingsService,
SettingsLoader,
NavigationLoader
]
});
let platform = TestBed.inject(Platform);
let router = TestBed.inject(Router);
sLoader = new SettingsFakeLoader();
nLoader = new NavigationFakeLoader();
sService = new SettingsService(sLoader, nLoader);
service = new ThemeConfigService(platform, router, sService);
});
it('should be created', () => {
expect(service).toBeTruthy();
});
});
There's a good example of what you're trying to do in the Angular testing documentation :在Angular 测试文档中有一个很好的例子说明你正在尝试做什么:
beforeEach(() => {
// stub UserService for test purposes
userServiceStub = {
isLoggedIn: true,
user: { name: 'Test User' },
};
TestBed.configureTestingModule({
declarations: [ WelcomeComponent ],
providers: [ { provide: UserService, useValue: userServiceStub } ],
// ^^^^^^ Note use of `useValue` ^^^^^^
});
fixture = TestBed.createComponent(WelcomeComponent);
comp = fixture.componentInstance;
// UserService from the root injector
userService = TestBed.inject(UserService);
// get the "welcome" element by CSS selector (e.g., by class name)
el = fixture.nativeElement.querySelector('.welcome');
});
You can make an instance of any class that implements your Service's public API, then provide that instance with useValue
when configuring the test module.您可以创建实现服务的公共 API 的任何 class 的实例,然后在配置测试模块时为该实例提供
useValue
。 Calling TestBed.inject(UserService)
will pass that value to the constructor of your dependent Service.调用
TestBed.inject(UserService)
会将该值传递给依赖服务的构造函数。
In your case, you can use jasmine.createSpyObj<SettingsLoader>("SettingsLoader", ["retrieveSettings"])
to create an instance that implements the SettingsLoader
API, then pass it to providers
as useValue
.在您的情况下,您可以使用
jasmine.createSpyObj<SettingsLoader>("SettingsLoader", ["retrieveSettings"])
创建一个实现SettingsLoader
API 的实例,然后将其作为useValue
传递给providers
。 Now, TestBed.inject(ThemeConfigService)
will call the real SettingsService
constructor, passing it the spy object you created, then call the ThemeConfigService
constructor, passing it the resulting SettingsService
instance.现在,
TestBed.inject(ThemeConfigService)
将调用真正的SettingsService
构造函数,将您创建的间谍 object 传递给它,然后调用ThemeConfigService
构造函数,将生成的SettingsService
实例传递给它。
It's probably a good idea to switch to this model instead of the one in your answer, because if in future you eg update your Platform
to depend on SettingsService
, you'd suddenly find yourself in a situation where part of your code is using DI ( platform
and router
are created with inject(...)
) but part of it isn't (you're new
-ing SettingsService
and ThemeConfigService
yourself).切换到此 model 而不是您答案中的那个可能是个好主意,因为如果将来您将
Platform
更新为依赖SettingsService
,您会突然发现自己处于部分代码使用 DI 的情况( platform
和router
是使用inject(...)
创建的)但其中一部分不是(您自己是new
的SettingsService
和ThemeConfigService
)。 Better to configure DI properly up front, and allow the injector to create all instances for you.最好预先正确配置 DI,并允许注入器为您创建所有实例。
Here I will develop a little Coderer answer - where instead useValue I use useClass在这里,我将开发一个小的Coderer 答案- 我使用 useClass 而不是 useValue
TestBed.configureTestingModule({
declarations: [...],
imports: [...],
providers: [
{ provide: SettingsLoader, useClass: SettingsFakeLoader },
...
],
schemas: [CUSTOM_ELEMENTS_SCHEMA],
})
So the pattern is: add to providers
section following service所以模式是:在服务之后添加到
providers
部分
{ provide: AbstractService, useClass: FakeService }
(where FakeService class is implementation of AbstractService class) (其中 FakeService class 是 AbstractService 类的实现)
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.