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

NGRX单元测试中overrideSelector方法未及时生效排查求助

时序问题分析:Effect中Store Selector值未及时更新

问题背景

遇到一个时序问题,无法理解成因。有一个Effect依赖从Store中通过selector赋值的类变量,相关代码如下:

Effects类构造函数

constructor(
    private actions$: Actions,
    private nifiService: NifiService,
    private store: Store<NiFiState>
  ) {
    this.store.pipe(select(selectProcessingGroupTransformProcessor))
    .subscribe((p) => {this.transformProcessor = <GenericProcessor>p});
  }

Selector定义

export const selectProcessingGroupTransformProcessor = createSelector(
  nifiState,
  (state: NiFiState) => state.processing.processors?.filter(p => p.component.type === environment.COT_TRANSFORM_PROCESSOR_TYPE)[0]
)

Effect代码

startTransformProcessor$ = createEffect(() =>
    this.actions$.pipe(
      ofType(
        ProcessingTransformActions.handleProcessingUpdateTransformSuccess
      ),
      mergeMap((action) =>{
        console.log('TRANSFORM PROC', this.transformProcessor)
        return this.nifiService
          .setProcessorState(this.transformProcessor.id, 'RUNNING')
          .pipe(
            map((resp) =>
              ProcessingTransformActions.handleStartTransformProcessorSuccess({
                processor: resp,
                gType: 'processing',
              })
            ),
            catchError((error) =>
              of(
                ProcessingTransformActions.handleStartTransformProcessorFailure(
                  { error }
                )
              )
            )
          )}
      )
    )
  );

失败的测试代码

it('[startTransformProcessor$] - handleProcessingUpdateTransformSuccess', () => {
        const action = handleProcessingUpdateTransformSuccess({ processor });
        const outcome = handleStartTransformProcessorSuccess({
          processor,
          gType: 'processing',
        });

        store.overrideSelector(
          selectProcessingGroupTransformProcessor,
          processor
        );
        store.refreshState();

        testScheduler.run(({ hot, cold, expectObservable }) => {
          actions$ = hot('-a', { a: action });
          const response = cold('-b|', { b: processor });
          spyObj = jest
            .spyOn(service, 'setProcessorState')
            .mockReturnValue(response);

          expectObservable(effects.startTransformProcessor$).toBe('--b', {
            b: outcome,
          });
        });
      });

日志输出顺序

console.log
    TRANSFORM PROC undefined

  console.log
    p {
      id: 'proc1',
      component: {
        id: 'proc1',
        config: { properties: [Object] },
        type: 'CoTtoNFFIProcessor'
      }
    }

  console.log
    p {
      id: 'proc1',
      component: {
        id: 'proc1',
        config: { properties: [Object] },
        type: 'CoTtoNFFIProcessor'
      }
    }

  console.log
    TRANSFORM PROC {
      id: 'proc1',
      component: {
        id: 'proc1',
        config: { properties: [Object] },
        type: 'CoTtoNFFIProcessor'
      }
    }

问题现象

  • 第一个测试中Effect的console.log()输出undefined,后续失败分支测试能正常输出mock的selector值
  • Effect的日志先于selector订阅的日志输出,说明Effect执行时transformProcessor还未被selector赋值更新
  • 将overrideSelector()移至beforeEach()中,结果无变化
  • 其他测试套件使用相同逻辑未出现问题

原因分析与解决方案

核心问题:订阅时机与测试调度的时序不匹配

  1. 构造函数订阅的执行时机:
    Effects实例创建时,构造函数中的store订阅会立即执行,但测试中overrideSelector和refreshState是在实例创建后调用的,导致第一次订阅的初始值为undefined。而testScheduler.run同步执行,Action触发时selector的更新还未完成。

  2. 测试中Store更新与Effect执行的时序:
    store.refreshState()触发的selector更新是异步的(基于RxJS异步调度),但testScheduler.run同步启动,Action触发会立即进入Effect处理逻辑,此时transformProcessor还未被更新。

解决方案

方案1:在Effect中直接获取Selector值(推荐)

放弃在构造函数中订阅selector并赋值给类变量,改用withLatestFrom在Effect内部实时获取最新状态,彻底避免时序问题:

startTransformProcessor$ = createEffect(() =>
    this.actions$.pipe(
      ofType(ProcessingTransformActions.handleProcessingUpdateTransformSuccess),
      withLatestFrom(this.store.pipe(select(selectProcessingGroupTransformProcessor))),
      mergeMap(([action, transformProcessor]) =>{
        console.log('TRANSFORM PROC', transformProcessor)
        return this.nifiService
          .setProcessorState(transformProcessor.id, 'RUNNING')
          .pipe(
            map((resp) =>
              ProcessingTransformActions.handleStartTransformProcessorSuccess({
                processor: resp,
                gType: 'processing',
              })
            ),
            catchError((error) =>
              of(
                ProcessingTransformActions.handleStartTransformProcessorFailure({ error })
              )
            )
          )}
      )
    )
  );

方案2:测试中等待Selector更新完成

如果必须保留类属性的方式,可在测试中手动等待selector的更新完成:

it('[startTransformProcessor$] - handleProcessingUpdateTransformSuccess', async () => {
        const action = handleProcessingUpdateTransformSuccess({ processor });
        const outcome = handleStartTransformProcessorSuccess({
          processor,
          gType: 'processing',
        });

        store.overrideSelector(selectProcessingGroupTransformProcessor, processor);
        store.refreshState();
        
        // 等待Selector订阅更新完成
        await store.pipe(select(selectProcessingGroupTransformProcessor), take(1)).toPromise();

        testScheduler.run(({ hot, cold, expectObservable }) => {
          actions$ = hot('-a', { a: action });
          const response = cold('-b|', { b: processor });
          spyObj = jest.spyOn(service, 'setProcessorState').mockReturnValue(response);

          expectObservable(effects.startTransformProcessor$).toBe('--b', { b: outcome });
        });
      });

方案3:实例创建前配置Selector

将overrideSelector移到Effects实例创建之前,确保构造函数订阅一开始就能拿到mock值:

beforeEach(() => {
    store.overrideSelector(selectProcessingGroupTransformProcessor, processor);
    store.refreshState();
    // 之后再创建Effects实例
    effects = new YourEffectsClass(actions$, service, store);
});

总结

最可靠的方案是方案1,直接在Effect中实时获取状态,既符合NgRx最佳实践,也彻底规避了异步时序问题。依赖类属性存储状态的方式本身就容易引发这类问题,因为状态更新和Effect执行的时机无法保证同步。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.06 01:31:17