使用抽象类依赖项测试Angular服务

时间:2020-04-17 18:50:24

标签: angular typescript unit-testing karma-jasmine

我有一个服务,该服务需要对另一个服务的依赖,而该服务又需要对两个抽象类的依赖。

(ThemeConfigService -> (SettingsService -> SettingsLoader, NavigationLoader))

到目前为止,由于无法找到通过抽象类公开的方法(不是函数异常),导致测试失败。

我不确定我将如何获得通过,事实证明,在线进行的各种搜索都没有帮助。

这是主题配置服务,我正在尝试为其充实测试“ theme-config.service.ts

@Injectable({
  providedIn: 'root'
})
export class ThemeConfigService {

  constructor(
    private platform: Platform,
    private router: Router,
    private settings: SettingsService
  ) {
    // code removed for brevity
  }
}

这是正在测试的服务“ 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();
  }
}

这里是SettingsLoader类,NavigationLoader看起来完全一样。从设计角度来看,它们必须是单独的类:

export abstract class SettingsLoader {
    abstract retrieveSettings(): Observable<any>;
}

我的单元测试看起来像这样:

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

业力返回的错误是:

TypeError: this.settingsLoader.retrieveSettings is not a function
which to me proves that it cannot resolve the abstract classes.

基于这个原因,我继续创建了以下内容:

export class SettingsFakeLoader extends SettingsLoader {
    retrieveSettings(): Observable<any> {
        return of({});
    }
}

并尝试使用它们更改SettingsLoaderNavigationLoader类的注入,然后Karma做出响应:

NullInjectorError: R3InjectorError(DynamicTestModule)[ThemeConfigService -> SettingsService -> SettingsLoader -> SettingsLoader]: 
  NullInjectorError: No provider for SettingsLoader!

beforeEach文件的修改后的theme-config.service.spec.ts

beforeEach(() => {
    TestBed.configureTestingModule({
        imports: [
        RouterModule,
        RouterTestingModule.withRoutes([])
        ],
        providers: [
        Platform,
        SettingsService,
        SettingsFakeLoader,
        NavigationFakeLoader
        ]
    });

    router = TestBed.inject(Router);
    service = TestBed.inject(ThemeConfigService);
});

通常,我不会尝试测试我认为如此复杂的内容。也许我只是没有看到“解决方案”。

任何帮助将不胜感激,因为我将有类似的情况来解决此应用程序开发的后续问题。

2 个答案:

答案 0 :(得分:0)

我走了实例化路线,而不是依赖于依赖注入。这可能不是一个可行的解决方案,但仍希望有更好方法的人来回答原始问题。

describe文件的更新后的theme-config.service.spe.ts

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

答案 1 :(得分:0)

Angular testing documentation中有一个很好的例子说明了您要做的事情:

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

您可以创建实现服务的公共API的任何类的实例,然后在配置测试模块时为该实例提供useValue。调用TestBed.inject(UserService)会将值传递给依赖服务的构造函数。

在您的情况下,您可以使用jasmine.createSpyObj<SettingsLoader>("SettingsLoader", ["retrieveSettings"])创建一个实现SettingsLoader API的实例,然后将其作为providers传递给useValue。现在,TestBed.inject(ThemeConfigService)将调用实际的SettingsService构造函数,并向其传递您创建的间谍对象,然后调用ThemeConfigService构造函数,并向其传递生成的SettingsService实例。

切换到此模型而不是回答中的模型可能是一个好主意,因为如果将来您例如更新Platform使其依赖于SettingsService,您会突然发现自己的部分代码正在使用DI({{1}和platform是使用{{ 1}}),但一部分不是(您是routerinject(...)自己)。最好预先正确配置DI,并允许注入器为您创建所有实例。