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

Angular组件Subscribe函数单元测试失败,求解决方案

Angular测试失败问题求助

我花了一周时间尝试自行解决问题但未成功,现求助。以下是待测试的HomePage组件代码、测试文件及模拟数据:

home.page.ts

import { Component, OnInit } from '@angular/core';
import { distinctUntilChanged } from 'rxjs/internal/operators/distinctUntilChanged';
import { AlertController, LoadingController } from '@ionic/angular';
import { DataService } from '../service/data.service';
import { League } from '../interfaces/League';
import { LeaguesImageLink } from '../types/leaguesImgLink.enum';
import { ActivatedRoute, Router } from '@angular/router';
// 注意:此处缺少ApiService的导入,需补充
// import { ApiService } from '../service/api.service';

@Component({
  selector: 'app-home',
  templateUrl: 'home.page.html',
  styleUrls: ['home.page.scss'],
})
export class HomePage implements OnInit {
  searchTerm: string = '';
  leaguesItems: League[] = [];

  constructor(
    private apiService: ApiService,
    private route: ActivatedRoute,
    private router: Router,
    private dataService: DataService,
    private loadingCtrl: LoadingController,
    private alertController: AlertController
  ) { }

  ngOnInit() {
    this.loadLeagues();
  }

  async loadLeagues(): Promise<void> {
    (await this.showSpinner()).present // 此处缺少(),应为present()
    this.apiService
      .getAllLeagues()
      .pipe(distinctUntilChanged())
      .subscribe((res: League[]) => {
        this.leaguesItems = res;
        this.addImgLink();
      });
    (await this.showSpinner()).dismiss() // 此处创建了新的加载实例,而非关闭之前的
  }

  async showSpinner(): Promise<HTMLIonLoadingElement> {
    const loading = await this.loadingCtrl.create({
      message: 'Loading..',
      spinner: 'bubbles',
    });
    return loading;
  }
  addImgLink(): void {
    this.leaguesItems = this.leaguesItems.map((item: League) => {
      return {
        ...item,
        imgLink:
          LeaguesImageLink[
          item.name.split(' ').join('') as keyof typeof LeaguesImageLink
          ],
      };
    });
  }

  async showAlert(): Promise<void> {
    const alert = await this.alertController.create({
      header: "Alert",
      message: "Il n'y a pas encore d'équipes !",
      buttons: ['OK'],
    });

    await alert.present();
  }

  setLeaguesData(league: League): void {
    if (league.teams?.length === 0) {
      this.showAlert()
      return;
    } else {
      this.dataService.setleaguesData(this.leaguesItems);
      this.router.navigate(['../teams', league._id], {
        relativeTo: this.route,
      });
    }
  }
}

home.page.spec.ts

import { MockLoadingController } from './../../mocks/IonicMock';
import { League } from './../interfaces/League';
import { mockLeagueArray, mockLeagueArrayImageLink } from './../../mocks/mockLeagues';
import { ApiService } from 'src/app/service/api.service';
import { FormsModule } from '@angular/forms';
import { Ng2SearchPipeModule } from 'ng2-search-filter/';
import { HttpClientTestingModule } from '@angular/common/http/testing';
import {
  ComponentFixture,
  fakeAsync,
  TestBed,
  tick,
  waitForAsync,
} from '@angular/core/testing';
import { RouterTestingModule } from '@angular/router/testing';

import { HomePage } from './home.page';
import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';

import { IonicModule } from '@ionic/angular';

import { of } from 'rxjs';

fdescribe('HomePage', () => {
  let component: HomePage;
  let fixture: ComponentFixture<HomePage>;
  let apiService: ApiService;

  beforeEach(async () => {
    await TestBed.configureTestingModule({
      declarations: [HomePage],
      teardown: { destroyAfterEach: false },
      schemas: [CUSTOM_ELEMENTS_SCHEMA],
      imports: [
        IonicModule.forRoot(),
        HttpClientTestingModule,
        RouterTestingModule,
        Ng2SearchPipeModule,
        FormsModule,
      ],
      providers: [
        ApiService
      ]
    }).compileComponents();

    fixture = TestBed.createComponent(HomePage);
    component = fixture.componentInstance;
    apiService = TestBed.inject(ApiService)

    fixture.detectChanges();
  });

  it('should be created', () => {
    expect(component).toBeTruthy();
  });

  it('should call ngOnInit', () => {
    let loadLeagues = spyOn(component, 'loadLeagues');
    component.ngOnInit();
    expect(loadLeagues).toHaveBeenCalled();
  });


  it('should call getAllLeagues service and fill leaguesItems array', fakeAsync(() => {
    //ARRANGE
    spyOn(apiService, 'getAllLeagues').and.returnValue(of(mockLeagueArray))

    //ACT
    component.loadLeagues()
    tick(1000)
    fixture.detectChanges()

    //ASSERT
    expect(component.leaguesItems).toEqual(mockLeagueArray)
  }))
});

mockLeagues.ts

import { League } from "src/app/interfaces/League";

export const mockLeagueArray: League[] = [{
  _id: "1",
  name: "Supercopa de Espana",
  sport: "sport1",
  teams: ["t1", "t11"],
}, {
  _id: "2",
  name: "English Premier League",
  sport: "sport2",
  teams: ["t2", "t22"],
}]

export const mockLeagueArrayImageLink: League[] = [{
  _id: "1",
  name: "Supercopa de Espana",
  sport: "sport1",
  teams: ["t1", "t11"],
  imgLink: "https://www.thesportsdb.com/images/media/league/badge/sp4q7d1641378531.png",
}, {
  _id: "2",
  name: "English Premier League",
  sport: "sport2",
  teams: ["t2", "t22"],
  imgLink: "https://www.thesportsdb.com/images/media/league/badge/pdd43f1610891709.png",
}]

运行ng test的报错信息

Chrome 107.0.0.0 (Windows 10) HomePage should call getAllLeagues service and fill leaguesItems array FAILED
        Expected $.length = 0 to equal 2.
        Expected $[0] = undefined to equal Object({ _id: '1', name: 'Supercopa de Espana', sport: 'sport1', teams: [ 't1', 't11' ] }).
        Expected $[1] = undefined to equal Object({ _id: '2', name: 'English Premier League', sport: 'sport2', teams: [ 't2', 't22' ] }).
            at <Jasmine>
            at UserContext.apply (src/app/home/home.page.spec.ts:81:36)
            at UserContext.apply (node_modules/zone.js/fesm2015/zone-testing.js:2030:30)
            at _ZoneDelegate.invoke (node_modules/zone.js/fesm2015/zone.js:372:26)
            at ProxyZoneSpec.onInvoke (node_modules/zone.js/fesm2015/zone-testing.js:287:39)
Chrome 107.0.0.0 (Windows 10): Executed 3 of 12 (1 FAILED) (skipped 9) (0.799 secs / 0.601 secs)
TOTAL: 1 FAILED, 2 SUCCESS

解决方案

问题根源

  1. 组件中loadLeagues方法的present()调用缺少括号,且两次调用showSpinner()创建了不同的加载实例,导致加载框逻辑混乱。
  2. 测试代码未处理loadLeagues异步方法中的微任务,导致apiService.getAllLeagues()的subscribe回调未及时执行,断言时leaguesItems仍为空。
  3. 组件中subscribe回调会调用addImgLink()修改leaguesItems,测试断言使用原始模拟数组会不匹配。

修改步骤

1. 修复组件代码(home.page.ts)
  • 补充ApiService的导入:
import { ApiService } from '../service/api.service';
  • 重构loadLeagues方法,复用加载实例并正确调用present():
async loadLeagues(): Promise<void> {
  const loading = await this.showSpinner();
  await loading.present();
  this.apiService
    .getAllLeagues()
    .pipe(distinctUntilChanged())
    .subscribe((res: League[]) => {
      this.leaguesItems = res;
      this.addImgLink();
      loading.dismiss();
    }, (error) => {
      loading.dismiss();
      // 可选:添加错误提示逻辑
    });
}
2. 修复测试代码(home.page.spec.ts)

选择以下两种方案之一:

方案一:验证包含imgLink的数组

it('should call getAllLeagues service and fill leaguesItems array', fakeAsync(() => {
  spyOn(apiService, 'getAllLeagues').and.returnValue(of(mockLeagueArray));

  component.loadLeagues();
  flushMicrotasks(); // 处理异步微任务,确保subscribe回调执行
  fixture.detectChanges();

  expect(component.leaguesItems).toEqual(mockLeagueArrayImageLink);
}))

方案二:阻止addImgLink修改数组,验证原始数据

it('should call getAllLeagues service and fill leaguesItems array', fakeAsync(() => {
  spyOn(apiService, 'getAllLeagues').and.returnValue(of(mockLeagueArray));
  spyOn(component, 'addImgLink'); // 拦截addImgLink调用,避免修改数组

  component.loadLeagues();
  flushMicrotasks();
  fixture.detectChanges();

  expect(component.leaguesItems).toEqual(mockLeagueArray);
  expect(component.addImgLink).toHaveBeenCalled(); // 验证addImgLink被触发
}))

内容的提问来源于stack exchange,提问作者Eric

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.10 12:45:33