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

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测试报错。

修复步骤

  1. 替换TestBed中的真实服务为模拟实例
    在测试配置中直接提供模拟的ServiceService,确保组件注入的是模拟服务而非真实服务。
  2. 修正测试断言
    原断言存在语法错误,同时补充更有意义的断言(如检查请求调用次数、数据是否正确存入数组)。

修改后的完整测试代码:

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.29 19:42:47