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

单元测试NGXS时如何触发selector订阅并解决forEach报错

问题说明

现有业务逻辑

  • 组件判断this.doctor不为undefined时,首先调用this.store.dispatch派发SearchTeamMemberData action
  • 随后订阅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值:
    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)
    });
    
    注意两个流都要mock,不要只mock外层流,内层isLoading$如果是undefined,同样会导致订阅逻辑执行异常。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.27 09:54:22