如何测试Angular组件中RxJS Tap内的属性变更?
Angular组件中RxJS Pipe/Tap操作的属性变更测试方案
问题背景
组件通过ngOnInit中的RxJS流加载分类数据,经过tap操作处理后更新两个BehaviorSubject:categories$和numberOfSelectedCategories$。测试时函数调用验证通过,但属性值未正确更新(numberOfSelectedCategories$始终为0)。
组件核心代码:
ngOnInit(): void { const getOne$ = this.appDataService.proxy.getAllWithoutPaginate().pipe( tap((result: Array<CategoryInterface>) => { const categories = []; result.forEach((category: CategoryInterface) => { this.parseCategoryTree(categories, category); }); this.categories$.next(categories); this.numberOfSelectedCategories$.next(this.calendarService.categories$.value.length); }) ); this.subscriptions.add(this.appDataService.start(getOne$, 'load categories').subscribe()); }
原有测试代码的核心问题:
// ... 省略导入 describe('CalendarHeaderComponent', () => { let component: CalendarHeaderComponent; let fixture: ComponentFixture<CalendarHeaderComponent>; const datas = CATEGORY_RESPONSE; beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ imports: [HttpClientTestingModule, DdataCoreModule.forRoot(environment), RouterTestingModule, AppModule], declarations: [CalendarHeaderComponent] }) .compileComponents() .then(() => { fixture = TestBed.createComponent(CalendarHeaderComponent); component = fixture.componentInstance; // 错误:组件初始化后才设置Spy,ngOnInit已执行,用的是真实服务 const proxySpy = jasmine.createSpyObj('ProxyService', ['getAllWithoutPaginate']); const appDataServiceSpy = jasmine.createSpyObj('AppDataService', ['proxy', 'start']); appDataServiceSpy.start.and.returnValue(of(datas)); proxySpy.getAllWithoutPaginate.and.returnValue(of(datas)); appDataServiceSpy.proxy = proxySpy; component.appDataService = appDataServiceSpy; fixture.detectChanges(); }); })); it('properties changing after load', fakeAsync(() => { // 错误:未处理异步流,且calendarService未Mock expect(component.numberOfSelectedCategories$.value).withContext('numberOfSelectedCategories$').toEqual(10); expect(component.categories$.value); })); });
问题分析
- Spy设置时机错误:
fixture.detectChanges()会触发ngOnInit,但此时Spy是在组件创建后才赋值,导致ngOnInit执行时使用真实服务而非Mock实例。 - 依赖Mock不完整:
numberOfSelectedCategories$的值依赖calendarService.categories$.value.length,原有测试未Mock该Observable,导致其值为空数组,长度为0。 - 未处理RxJS异步流:使用
fakeAsync但未调用flush()/tick(),RxJS流未完成,tap中的逻辑未触发。
修复步骤
1. 提前在TestBed中注入Mock服务
将Spy服务配置到TestBed的providers中,确保组件初始化时直接使用Mock实例:
beforeEach(waitForAsync(() => { // 提前创建并配置Spy const proxySpy = jasmine.createSpyObj('ProxyService', ['getAllWithoutPaginate']); const appDataServiceSpy = jasmine.createSpyObj('AppDataService', ['start']); appDataServiceSpy.proxy = proxySpy; proxySpy.getAllWithoutPaginate.and.returnValue(of(datas)); appDataServiceSpy.start.and.callFake((obs$) => obs$); // 模拟start转发Observable的行为 // Mock calendarService的categories$,返回处理后的10个分类 const mockCategories = datas.flatMap(cat => [cat, ...(cat.children || [])]); const calendarServiceSpy = jasmine.createSpyObj('CalendarService', [], { categories$: new BehaviorSubject(mockCategories) }); TestBed.configureTestingModule({ imports: [HttpClientTestingModule, RouterTestingModule], declarations: [CalendarHeaderComponent], providers: [ { provide: AppDataService, useValue: appDataServiceSpy }, { provide: CalendarService, useValue: calendarServiceSpy } ] }).compileComponents().then(() => { fixture = TestBed.createComponent(CalendarHeaderComponent); component = fixture.componentInstance; fixture.detectChanges(); // 此时ngOnInit使用Mock服务执行 }); }));
2. 在测试用例中触发异步流完成
在fakeAsync测试中调用flush(),确保所有RxJS异步操作执行完毕:
it('properties changing after load', fakeAsync(() => { flush(); // 触发所有异步流完成 // 验证categories$处理后的长度 expect(component.categories$.value.length).withContext('categories$ length').toEqual(10); // 验证选中分类数量 expect(component.numberOfSelectedCategories$.value).withContext('numberOfSelectedCategories$').toEqual(10); }));
3. 可选:验证分类树处理逻辑
如果需要确保parseCategoryTree的正确性,可以增加结构验证:
it('parses category tree correctly', fakeAsync(() => { flush(); const processedCategories = component.categories$.value; // 验证父分类和子分类数量匹配测试数据 expect(processedCategories.filter(c => !c.parentId).length).toEqual(6); expect(processedCategories.filter(c => c.parentId).length).toEqual(4); }));
完整修复后的测试代码示例
import { fakeAsync, flush, ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; import { HttpClientTestingModule } from '@angular/common/http/testing'; import { RouterTestingModule } from '@angular/router/testing'; import { BehaviorSubject, of } from 'rxjs'; import { CalendarHeaderComponent } from './calendar-header.component'; import { AppDataService } from 'path-to-app-data-service'; import { CalendarService } from 'path-to-calendar-service'; import { CATEGORY_RESPONSE } from 'path-to-test-data'; describe('CalendarHeaderComponent', () => { let component: CalendarHeaderComponent; let fixture: ComponentFixture<CalendarHeaderComponent>; const datas = CATEGORY_RESPONSE; beforeEach(waitForAsync(() => { const proxySpy = jasmine.createSpyObj('ProxyService', ['getAllWithoutPaginate']); const appDataServiceSpy = jasmine.createSpyObj('AppDataService', ['start']); appDataServiceSpy.proxy = proxySpy; proxySpy.getAllWithoutPaginate.and.returnValue(of(datas)); appDataServiceSpy.start.and.callFake((obs$) => obs$); const mockCategories = datas.flatMap(cat => [cat, ...(cat.children || [])]); const calendarServiceSpy = jasmine.createSpyObj('CalendarService', [], { categories$: new BehaviorSubject(mockCategories) }); TestBed.configureTestingModule({ imports: [HttpClientTestingModule, RouterTestingModule], declarations: [CalendarHeaderComponent], providers: [ { provide: AppDataService, useValue: appDataServiceSpy }, { provide: CalendarService, useValue: calendarServiceSpy } ] }).compileComponents().then(() => { fixture = TestBed.createComponent(CalendarHeaderComponent); component = fixture.componentInstance; fixture.detectChanges(); }); })); it('calls service methods on init', fakeAsync(() => { flush(); expect(component.appDataService.proxy.getAllWithoutPaginate).toHaveBeenCalledTimes(1); expect(component.appDataService.start).toHaveBeenCalledTimes(1); })); it('updates properties after data load', fakeAsync(() => { flush(); expect(component.categories$.value.length).toEqual(10); expect(component.numberOfSelectedCategories$.value).toEqual(10); })); });
内容的提问来源于stack exchange,提问作者Ferenc Bablena
相关产品推荐
相关产品推荐

