Angular单元测试报错:fake async测试内无法发起XHR请求
问题:Angular单元测试调用loadNews()时触发XHR请求错误
测试Homepage组件的loadNews()函数时,抛出以下错误:
Error: Cannot make XHRs from within a fake async test. Request URL: https://hacker-news.firebaseio.com/v0/item/4657586786.json
相关代码
HomepageComponent.ts
import { Component, OnInit } from '@angular/core'; import { ServiceService } from '../service.service'; @Component({ selector: 'app-homepage', templateUrl: './homepage.component.html', styleUrls: ['./homepage.component.css'], }) export class HomepageComponent implements OnInit { allNewsId: Array<number>; newsId: Array<number>; news: Array<any> = []; i = 0; constructor(private service: ServiceService) {} ngOnInit(): void { this.service.getAllNews().subscribe((data: any) => { this.allNewsId = data; this.loadNews(); }); } loadNews() { this.newsId = this.allNewsId.slice(0, this.i + 10); while (this.i < this.newsId.length) { this.service.getOneNews(this.newsId[this.i]).subscribe((el) => { this.news.push(el); }); this.i++; } } addNews() { this.loadNews(); } }
ServiceService.ts
import { Injectable } from '@angular/core'; import { HttpClient } from '@angular/common/http'; @Injectable({ providedIn: 'root', }) export class ServiceService { constructor(private http: HttpClient) {} getAllNews() { return this.http.get( 'https://hacker-news.firebaseio.com/v0/newstories.json' ); } getOneNews(id: number) { return this.http.get( `https://hacker-news.firebaseio.com/v0/item/${String(id)}.json` ); } }
HomepageComponent.spec.ts
import { ComponentFixture, fakeAsync, flush, TestBed, } from '@angular/core/testing'; import { ServiceService } from '../service.service'; import { HomepageComponent } from './homepage.component'; import { HttpClientModule } from '@angular/common/http'; import { BrowserModule } from '@angular/platform-browser'; import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'; import { of } from 'rxjs'; describe('HomepageComponent', () => { let component: HomepageComponent; let fixture: ComponentFixture<HomepageComponent>; let service: ServiceService; let httpClientSpy: { get: jasmine.Spy }; beforeEach(async () => { await TestBed.configureTestingModule({ declarations: [HomepageComponent], imports: [BrowserModule, HttpClientModule], providers: [ServiceService], schemas: [CUSTOM_ELEMENTS_SCHEMA], }).compileComponents(); fixture = TestBed.createComponent(HomepageComponent); component = fixture.componentInstance; httpClientSpy = jasmine.createSpyObj('HttpClient', ['get']); service = new ServiceService(httpClientSpy as any); fixture.detectChanges(); }); it('should create', () => { expect(component).toBeTruthy(); }); it('loadNews()', fakeAsync(() => { const newsSample = { by: '', descendants: 0, id: 768678, score: 1, time: 123454345654, title: '', type: '', url: '', }; component.allNewsId = [67854, 67546, 456789654, 63457687985746, 4657586786]; spyOn(service, 'getOneNews').and.returnValue(of(newsSample)); component.loadNews(); flush(); expect(component.loadNews).toHaveBeenCalled; }); });
问题原因与修复方案
原因
测试代码中手动创建了模拟service实例,但组件实际使用的是TestBed注入的真实ServiceService实例。调用component.loadNews()时,组件会触发真实服务的getOneNews方法,发起真实XHR请求,导致fakeAsync测试报错。
修复步骤
- 替换TestBed中的真实服务为模拟实例
在测试配置中直接提供模拟的ServiceService,确保组件注入的是模拟服务而非真实服务。 - 修正测试断言
原断言存在语法错误,同时补充更有意义的断言(如检查请求调用次数、数据是否正确存入数组)。
修改后的完整测试代码:
import { ComponentFixture, fakeAsync, flush, TestBed, } from '@angular/core/testing'; import { ServiceService } from '../service.service'; import { HomepageComponent } from './homepage.component'; import { HttpClientModule } from '@angular/common/http'; import { BrowserModule } from '@angular/platform-browser'; import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'; import { of } from 'rxjs'; describe('HomepageComponent', () => { let component: HomepageComponent; let fixture: ComponentFixture<HomepageComponent>; let service: jasmine.SpyObj<ServiceService>; beforeEach(async () => { // 创建ServiceService的模拟对象 const serviceSpy = jasmine.createSpyObj('ServiceService', ['getAllNews', 'getOneNews']); await TestBed.configureTestingModule({ declarations: [HomepageComponent], imports: [BrowserModule, HttpClientModule], // 用模拟对象替换真实服务 providers: [{ provide: ServiceService, useValue: serviceSpy }], schemas: [CUSTOM_ELEMENTS_SCHEMA], }).compileComponents(); fixture = TestBed.createComponent(HomepageComponent); component = fixture.componentInstance; // 获取注入的模拟服务 service = TestBed.inject(ServiceService) as jasmine.SpyObj<ServiceService>; // 模拟getAllNews的返回,避免ngOnInit发起真实请求 service.getAllNews.and.returnValue(of([])); fixture.detectChanges(); }); it('should create', () => { expect(component).toBeTruthy(); }); it('loadNews() should fetch and add news to news array', fakeAsync(() => { const newsSample = { by: '', descendants: 0, id: 768678, score: 1, time: 123454345654, title: '', type: '', url: '', }; component.allNewsId = [67854, 67546, 456789654, 63457687985746, 4657586786]; // 模拟getOneNews的返回 service.getOneNews.and.returnValue(of(newsSample)); component.loadNews(); flush(); // 断言getOneNews被调用了5次(对应allNewsId的长度) expect(service.getOneNews).toHaveBeenCalledTimes(5); // 断言news数组长度为5 expect(component.news.length).toBe(5); // 断言数组中的元素是模拟的newsSample expect(component.news.every(item => item.id === newsSample.id)).toBeTrue(); })); });
额外优化建议
组件中loadNews用while循环发起多个独立订阅,可改用forkJoin合并Observable,便于统一处理和测试:
import { forkJoin } from 'rxjs'; // ... loadNews() { this.newsId = this.allNewsId.slice(0, this.i + 10); const newsObservables = this.newsId.slice(this.i).map(id => this.service.getOneNews(id)); forkJoin(newsObservables).subscribe(newsList => { this.news.push(...newsList); this.i = this.newsId.length; }); }
内容的提问来源于stack exchange,提问作者Alberto Piazzini
相关产品推荐
相关产品推荐

