You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.13 03:17:33