简体   繁体   English

使用抽象 class 依赖项测试 Angular 服务的问题

[英]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:并尝试使用这些更改SettingsLoaderNavigationLoader类的注入,然后 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 的情况( platformrouter是使用inject(...)创建的)但其中一部分不是(您自己是newSettingsServiceThemeConfigService )。 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.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM