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()中,结果无变化 - 其他测试套件使用相同逻辑未出现问题
原因分析与解决方案
核心问题:订阅时机与测试调度的时序不匹配
构造函数订阅的执行时机:
Effects实例创建时,构造函数中的store订阅会立即执行,但测试中overrideSelector和refreshState是在实例创建后调用的,导致第一次订阅的初始值为undefined。而testScheduler.run同步执行,Action触发时selector的更新还未完成。测试中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
相关产品推荐
相关产品推荐

