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
解决方案
问题根源
- 组件中
loadLeagues方法的present()调用缺少括号,且两次调用showSpinner()创建了不同的加载实例,导致加载框逻辑混乱。 - 测试代码未处理
loadLeagues异步方法中的微任务,导致apiService.getAllLeagues()的subscribe回调未及时执行,断言时leaguesItems仍为空。 - 组件中
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
相关产品推荐
相关产品推荐

