单元测试NGXS时如何触发selector订阅并解决forEach报错
问题说明
现有业务逻辑
- 组件判断
this.doctor不为undefined时,首先调用this.store.dispatch派发SearchTeamMemberDataaction - 随后订阅NGXS selector对应的可观察对象
teamMemberService$,拿到返回列表后遍历所有元素:根据元素id是否不等于0,将元素分别归入this.teamList或this.listZoroh - 在上述
teamMemberService$的订阅回调内,再订阅isLoading$selector,将返回值赋值给this.isLoading,控制页面加载状态
测试遇到的问题
- 测试初始逻辑:构造测试用doctor对象赋值给组件实例,用
jest.spyOn监听component.teamMemberDetails$的subscribe方法,调用component.getView()后执行fixture.detectChanges()触发变更检测,期望验证selector订阅被正常调用 - 已尝试方案:通过
Object.defineProperty将component.teamMemberDetails$重写为可写属性,赋值为of([{ id: 1 }])mock selector返回值,期望触发订阅拿到列表数据 - 报错信息:运行测试时抛出
TypeError: Cannot read property 'forEach' of undefined错误,始终无法进入selector订阅逻辑完成测试
可行实现方案
- 优先用NGXS官方测试工具MockSelector,不要手动重写组件实例属性
手动重写属性很容易出现时序问题:组件初始化逻辑先执行,mock值后挂载,导致订阅拿到的还是原始的undefined值。直接在测试模块配置阶段就mock掉所有用到的selector返回值,从数据源层面就替换掉真实selector:import { NgxsModule, MockSelector } from '@ngxs/store/testing'; // 导入业务实际用到的状态类 import { TeamState } from './team.state'; beforeEach(async () => { await TestBed.configureTestingModule({ imports: [NgxsModule.forRoot([TeamState])], declarations: [YourTargetComponent], providers: [ // 把业务用到的两个selector全部mock,返回提前定义好的测试数据流 MockSelector(TeamState.teamMemberService$, of([{ id: 1 }, { id: 0 }])), MockSelector(TeamState.isLoading$, of(false)) ] }).compileComponents(); }); - 核对属性名拼写一致性
业务代码订阅的流是teamMemberService$,测试里监听的是teamMemberDetails$,先确认组件内实际订阅的属性名是否和mock的属性名完全一致,属性名不匹配会直接导致订阅拿到undefined,触发forEach报错。 - 修正异步时序
所有涉及RxJS订阅的测试,统一用fakeAsync包裹测试用例,调用fixture.detectChanges()之后执行tick()排空所有异步队列,再执行断言,避免订阅回调还没执行就跑校验逻辑:it('should get team list when doctor exists', fakeAsync(() => { component.doctor = { /* 测试用doctor对象 */ }; const subscribeSpy = jest.spyOn(component.teamMemberService$, 'subscribe'); component.getView(); fixture.detectChanges(); tick(); // 排空所有微任务、订阅回调 expect(subscribeSpy).toHaveBeenCalled(); expect(component.teamList).toEqual([{id: 1}]); expect(component.listZoroh).toEqual([{id: 0}]); expect(component.isLoading).toBe(false); })); - 如果一定要用手动重写属性的方式,把重写逻辑前移到组件实例创建之后、调用任何组件方法之前,重写时要保证属性可枚举、可配置,避免组件初始化时拿不到mock值:
注意两个流都要mock,不要只mock外层流,内层component = fixture.componentInstance; // 实例创建完立刻重写,不要等调用getView之后再写 Object.defineProperty(component, 'teamMemberService$', { writable: true, value: of([{id: 1}, {id: 0}]) }); Object.defineProperty(component, 'isLoading$', { writable: true, value: of(false) });isLoading$如果是undefined,同样会导致订阅逻辑执行异常。
内容的提问来源于stack exchange,提问作者fadingbeat
相关产品推荐
相关产品推荐

