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

Angular中含RxJS race操作符的NgRx Effects单元测试咨询

NgRx Effects含race操作符的单元测试方案

针对你描述的使用switchMap嵌套race(监听3秒timer或指定action)的Effects,以下是可行的单元测试写法,分fakeAsync同步测试和Marble事件流测试两种方案:

先明确Effects示例代码(对应你的场景)

假设你的Effects逻辑如下:

import { Injectable } from '@angular/core';
import { Actions, createEffect, ofType } from '@ngrx/effects';
import { of, race, timer } from 'rxjs';
import { map, switchMap } from 'rxjs/operators';

// 假设的Action类型
export enum MyActionTypes {
  Init = '[My] Init',
  TriggerAction = '[My] Trigger Action',
  FinalAction = '[My] Final Action',
}

export const init = () => ({ type: MyActionTypes.Init });
export const triggerAction = () => ({ type: MyActionTypes.TriggerAction });
export const finalAction = (trigger: 'timer' | 'action') => ({
  type: MyActionTypes.FinalAction,
  payload: { trigger },
});

@Injectable()
export class MyEffects {
  myEffect$ = createEffect(() =>
    this.actions$.pipe(
      ofType(MyActionTypes.Init),
      switchMap(() =>
        race(
          timer(3000).pipe(mapTo('timer')),
          this.actions$.pipe(ofType(MyActionTypes.TriggerAction), mapTo('action'))
        ).pipe(
          switchMap((trigger) => of(finalAction(trigger)))
        )
      )
    )
  );

  constructor(private actions$: Actions) {}
}

方案1:fakeAsync同步测试

这种方式适合直观测试时间推进和action触发场景:

import { fakeAsync, flush, tick } from '@angular/core/testing';
import { TestBed } from '@angular/core/testing';
import { provideMockActions } from '@ngrx/effects/testing';
import { Observable, of, concat } from 'rxjs';
import { Action } from '@ngrx/store';
import { MyEffects, init, triggerAction, finalAction } from './my.effects';

describe('MyEffects', () => {
  let actions$: Observable<Action>;
  let effects: MyEffects;

  beforeEach(() => {
    TestBed.configureTestingModule({
      providers: [
        MyEffects,
        provideMockActions(() => actions$), // 模拟Action流
      ],
    });

    effects = TestBed.inject(MyEffects);
  });

  // 测试:3秒timer先触发,派发FinalAction
  it('dispatches FinalAction when timer completes first', fakeAsync(() => {
    // 触发初始Init Action
    actions$ = of(init());

    let emittedAction: Action | undefined;
    // 必须订阅Effect流,否则内部逻辑不会执行
    effects.myEffect$.subscribe((action) => {
      emittedAction = action;
    });

    // 快进3秒,触发timer
    tick(3000);

    // 断言结果
    expect(emittedAction).toEqual(finalAction('timer'));
  }));

  // 测试:TriggerAction先派发,立即触发FinalAction
  it('dispatches FinalAction when TriggerAction is emitted first', fakeAsync(() => {
    // 先发出Init,紧接着发出TriggerAction
    actions$ = concat(of(init()), of(triggerAction()));

    let emittedAction: Action | undefined;
    effects.myEffect$.subscribe((action) => {
      emittedAction = action;
    });

    // 刷新所有异步任务(处理action流的订阅)
    flush();

    expect(emittedAction).toEqual(finalAction('action'));
  }));
});

方案2:Marble事件流测试(NgRx推荐)

使用TestScheduler可以更精准地模拟时间和事件流,适合复杂的RxJS逻辑测试:

import { TestBed } from '@angular/core/testing';
import { provideMockActions } from '@ngrx/effects/testing';
import { Observable } from 'rxjs';
import { Action } from '@ngrx/store';
import { TestScheduler } from 'rxjs/testing';
import { MyEffects, init, triggerAction, finalAction } from './my.effects';

describe('MyEffects (Marble Tests)', () => {
  let actions$: Observable<Action>;
  let effects: MyEffects;
  let scheduler: TestScheduler;

  beforeEach(() => {
    TestBed.configureTestingModule({
      providers: [
        MyEffects,
        provideMockActions(() => actions$),
      ],
    });

    effects = TestBed.inject(MyEffects);
    // 初始化测试调度器,用于断言事件流
    scheduler = new TestScheduler((actual, expected) => {
      expect(actual).toEqual(expected);
    });
  });

  it('emits FinalAction after 3s when timer wins the race', () => {
    scheduler.run(({ hot, expectObservable }) => {
      const initAction = init();
      const expectedFinalAction = finalAction('timer');

      // 模拟:0时刻发出Init Action
      actions$ = hot('-a', { a: initAction });

      // 断言:3秒后发出FinalAction
      expectObservable(effects.myEffect$).toBe('3s b', { b: expectedFinalAction });
    });
  });

  it('emits FinalAction immediately when TriggerAction wins the race', () => {
    scheduler.run(({ hot, expectObservable }) => {
      const initAction = init();
      const triggerAct = triggerAction();
      const expectedFinalAction = finalAction('action');

      // 模拟:0时刻发出Init,10ms后发出TriggerAction
      actions$ = hot('-a-b', { a: initAction, b: triggerAct });

      // 断言:TriggerAction发出后立即发出FinalAction
      expectObservable(effects.myEffect$).toBe('--c', { c: expectedFinalAction });
    });
  });
});

关键注意事项

  • 必须订阅Effect流:Effects是冷Observable,只有订阅后才会执行内部逻辑,这是很多测试失败的核心原因
  • fakeAsync的正确使用:tick(3000)用于快进时间触发timer,flush()用于处理所有待执行的异步任务(比如action流的订阅)
  • Marble测试优势:可以精准控制事件的时间顺序,避免fakeAsync中可能出现的异步时序问题,NgRx官方更推荐这种方式

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.28 17:55:01