Angular Test Harness因API超时配置导致测试超时失败求助
单元测试超时问题:含timeout操作符的服务导致Jasmine超时错误
我使用Test Harness对ConsolidatedLinkedIssuesComponent进行单元测试时遇到如下问题:当RtQueryServiceService的getLinkedIssesData方法配置了timeout操作符后,测试触发Jasmine超时错误:
Error: Timeout - Async function did not complete within 5000ms (set by jasmine.DEFAULT_TIMEOUT_INTERVAL)
注释timeout配置后测试可正常通过,但业务需求要求保留该超时逻辑(用于API响应超时时展示错误提示)。我已尝试在beforeEach中mock服务返回空数组,但问题仍未解决,寻求解决方案。
单元测试代码
beforeEach(() => { TestBed.configureTestingModule({ imports: [ HttpClientTestingModule, BrowserAnimationsModule, MatSelectModule, MatTableModule ], providers: [ { provide: ActivatedRoute, useValue:{ paramMap: of(params) } } ], declarations: [ ConsolidatedLinkedIssuesComponent, ] }); fixture = TestBed.createComponent(ConsolidatedLinkedIssuesComponent); loader = TestbedHarnessEnvironment.loader(fixture); service = TestBed.inject(RtQueryServiceService); TestBed.inject(ActivatedRoute); component = fixture.componentInstance; fixture.detectChanges(); }); it('sample ut', async () => { // mocking service spyOn(service, 'getLinkedIssesData').and.returnValue(of(rtConsolidatedLinkedIssesTableData)); // initializing component component.ngOnInit(); const issueTypeFilter = await loader.getHarness<MatSelectHarness>(MatSelectHarness.with({ selector: '#issuetype-filter'})); const table = await loader.getHarness<MatTableHarness>(MatTableHarness.with({selector: '#data-table'})); await issueTypeFilter.clickOptions(); expect((await table.getRows()).length).toBe(3); });
服务方法代码
public getLinkedIssesData(releaseName: string): Observable<RtLinkedIssuesDetails[]>{ console.log('RtQueryServiceService:getLinkedIssesData request for release:', releaseName); return this.http.get<any>( RestServerRT.CONSOLIDATED_LINKED_ISSUED_DATA.replace('{release-name}', releaseName), {headers: this.headers}) .pipe( timeout({ each: API_CALL_TIMEOUT_MS, with: () => throwError(() => new RtDbQueryTimeout('more than:'+API_CALL_TIMEOUT_MS+'ms')) }), catchError(err => { console.error('Issue seen in endpoint:', RestServerRT.CONSOLIDATED_LINKED_ISSUED_DATA, ', details:', err); return throwError( () => new RtRestApiCallException('Issue seen in endpoint: '+ RestServerRT.CONSOLIDATED_LINKED_ISSUED_DATA)); }), map(linkedIssuesData => this.generateRtLinkedIssuesDetailsObjects(linkedIssuesData)) ); }
组件ngOnInit代码
ngOnInit(): void { this.route.paramMap.subscribe( params => { this.releaseName = params.get(URL_QUERY_PARAMS_KEY.RELEASE_NAME); console.log('ConsolidatedLinkedIssuesComponent: Received query params, releaseName:',this.releaseName); this.isQueryInProgress = true; this.rtQUeryServiceSub$ = this.rtQUeryService.getLinkedIssesData(this.releaseName).subscribe( { next: (data) => { this.linkedIssuesData = data; console.log('ConsolidatedLinkedIssuesComponent: Received data from service:', this.linkedIssuesData); if(data.length === 0){ this.isEmptyData = true; this.isQueryInProgress = false; } else { this.generateConsolidatedLinkedData(data); // Generating formatted data this.generateDropdowns(this.consolidatedLinkedIssuesData); // Generating dropdowns this.dataSource = new MatTableDataSource( this.consolidatedLinkedIssuesData); // Mat data source initialization this.dataSource.sort = this.sort; this.isQueryInProgress = false; } this.cd.markForCheck(); // marking for change in upcoming CD } , error: (error) => { console.error('ConsolidatedLinkedIssuesComponent: Issue seen in getting data from service, details:',error); this.isQueryInProgress = false; this.isDataError = true; this.cd.markForCheck(); // marking for change in upcoming CD } } ); } ); }
更新后的beforeEach代码
beforeEach(() => { TestBed.configureTestingModule({ imports: [ HttpClientTestingModule, BrowserAnimationsModule, MatSelectModule, MatTableModule ], providers: [ { provide: ActivatedRoute, useValue:{ paramMap: of(params) } } ], declarations: [ ConsolidatedLinkedIssuesComponent, ] }); fixture = TestBed.createComponent(ConsolidatedLinkedIssuesComponent); loader = TestbedHarnessEnvironment.loader(fixture); service = TestBed.inject(RtQueryServiceService); serviceSpy = spyOn(service, 'getLinkedIssesData').and.returnValue(of([])); // <<----------- TestBed.inject(ActivatedRoute); component = fixture.componentInstance; fixture.detectChanges(); });
解决方案
1. 确保Mock服务完全覆盖原始实现
问题根源可能是spyOn没有完全拦截原始服务方法,导致实际执行了含timeout的HTTP请求逻辑(测试环境中未flush的请求会挂起,触发超时)。更可靠的方式是在TestBed中直接提供Mock服务:
修改TestBed配置的providers数组:
providers: [ { provide: ActivatedRoute, useValue:{ paramMap: of(params) } }, // 新增Mock服务配置 { provide: RtQueryServiceService, useValue: { getLinkedIssesData: jasmine.createSpy('getLinkedIssesData').and.returnValue(of([])) } } ],
这样组件注入的是完全Mock的服务,不会执行原始服务的HTTP请求和timeout逻辑。
2. 等待异步操作与变更检测完成
测试中调用ngOnInit后,需等待组件完成数据处理和UI更新,再操作Test Harness元素:
修改测试用例:
it('sample ut', async () => { const mockService = TestBed.inject(RtQueryServiceService); // 更新Mock返回数据 mockService.getLinkedIssesData.and.returnValue(of(rtConsolidatedLinkedIssesTableData)); component.ngOnInit(); // 等待异步操作完成 await fixture.whenStable(); // 手动触发变更检测确保UI更新 fixture.detectChanges(); const issueTypeFilter = await loader.getHarness<MatSelectHarness>(MatSelectHarness.with({ selector: '#issuetype-filter'})); const table = await loader.getHarness<MatTableHarness>(MatTableHarness.with({selector: '#data-table'})); await issueTypeFilter.clickOptions(); expect((await table.getRows()).length).toBe(3); });
3. 测试超时逻辑(可选)
若需验证超时错误处理,可Mock一个延迟超过API_CALL_TIMEOUT_MS的Observable,并调整Jasmine超时阈值:
it('should handle timeout error', async () => { // 调整Jasmine超时时间 jasmine.DEFAULT_TIMEOUT_INTERVAL = API_CALL_TIMEOUT_MS + 1000; const mockService = TestBed.inject(RtQueryServiceService); // 返回延迟Observable触发timeout mockService.getLinkedIssesData.and.returnValue( of(rtConsolidatedLinkedIssesTableData).pipe(delay(API_CALL_TIMEOUT_MS + 100)) ); component.ngOnInit(); await fixture.whenStable(); fixture.detectChanges(); expect(component.isDataError).toBe(true); expect(component.isQueryInProgress).toBe(false); });
内容的提问来源于stack exchange,提问作者Arnab Mukherjee
相关产品推荐
相关产品推荐

