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

如何优雅测试使用依赖注入服务事件的NgRx Effect

这个问题确实是NgRx Effect测试中常见的痛点——因为Effect在实例化时就会立即订阅依赖服务的Observable,导致我们没法在测试中动态修改spy的返回值来触发不同场景。下面有几个优雅的解决方案,既符合AAA测试模式,又能保持测试的简洁性:


方案一:用BehaviorSubject动态控制数据流

这是最直接的解决方案,我们让Spy返回一个BehaviorSubject,这样就能在测试的Act阶段主动推送事件,完全符合AAA的分离原则。

import { BehaviorSubject } from 'rxjs';
import { PlatformEffects } from './platform.effects';
import { PlatformService } from './platform.service';
import { PlatformActions } from './platform.actions';
import { TestBed } from '@angular/core/testing';

describe('PlatformEffects', () => {
  let effects: PlatformEffects;
  let platformServiceSpy: jasmine.SpyObj<PlatformService>;
  let resumeSubject: BehaviorSubject<void>;

  beforeEach(() => {
    // 创建BehaviorSubject作为resume方法的返回流
    resumeSubject = new BehaviorSubject<void>(undefined);
    platformServiceSpy = jasmine.createSpyObj('PlatformService', {
      resume: resumeSubject.asObservable()
    });

    TestBed.configureTestingModule({
      providers: [
        PlatformEffects,
        { provide: PlatformService, useValue: platformServiceSpy }
      ]
    });

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

  it('should be created', () => {
    expect(effects).toBeTruthy();
  });

  describe('resume$', () => {
    it('dispatches appResumed action on resume event', (done) => {
      // Arrange:订阅Effect的输出,准备断言
      effects.resume$.subscribe({
        next: (action) => {
          expect(action).toEqual(PlatformActions.appResumed());
          done();
        },
        error: done.fail
      });

      // Act:推送事件触发Effect
      resumeSubject.next();
    });
  });
});

为什么这有效?
BehaviorSubject是一个可推送值的Observable,我们在测试前让Spy返回它的订阅流,后续在测试中通过next()方法主动触发事件,完全不需要修改Effect的初始化逻辑,完美符合AAA模式。


方案二:使用NgRx官方TestScheduler(推荐复杂场景)

如果你的Effect涉及更复杂的异步逻辑(比如延迟、多个事件序列),NgRx提供的TestScheduler可以让你同步测试异步流,避免使用done()回调,代码更简洁可控。

import { TestScheduler } from '@ngrx/effects/testing';
import { PlatformEffects } from './platform.effects';
import { PlatformService } from './platform.service';
import { PlatformActions } from './platform.actions';

describe('PlatformEffects', () => {
  let platformServiceSpy: jasmine.SpyObj<PlatformService>;
  let testScheduler: TestScheduler;

  beforeEach(() => {
    testScheduler = new TestScheduler((actual, expected) => {
      // 自定义断言逻辑,默认复用Jasmine的expect
      expect(actual).toEqual(expected);
    });

    platformServiceSpy = jasmine.createSpyObj('PlatformService', ['resume']);
  });

  // 辅助函数:创建新的Effect实例(避免全局实例的订阅污染)
  const createEffects = () => new PlatformEffects(platformServiceSpy);

  describe('resume$', () => {
    it('dispatches appResumed action on resume event', () => {
      testScheduler.run((helpers) => {
        const { cold, expectObservable } = helpers;

        // Arrange:模拟resume方法返回的事件流
        const resumeEvent$ = cold('-a|', { a: undefined });
        platformServiceSpy.resume.and.returnValue(resumeEvent$);

        // Act & Assert:验证Effect的输出序列
        const effects = createEffects();
        expectObservable(effects.resume$).toBe('-b|', { b: PlatformActions.appResumed() });
      });
    });
  });
});

为什么这有效?
TestScheduler会同步执行所有异步流,我们用cold()创建模拟的事件序列,然后通过expectObservable()直接断言Effect的输出,完全不需要处理异步回调,适合复杂的Effect场景。


方案三:封装实例创建逻辑(简化重复代码)

如果你不想用RxJS的Subject,也可以通过封装辅助函数,每次测试都创建全新的Effect实例,确保Spy的返回值是当前测试的预期值,避免全局实例的订阅污染。

import { of } from 'rxjs';
import { PlatformEffects } from './platform.effects';
import { PlatformService } from './platform.service';
import { PlatformActions } from './platform.actions';

describe('PlatformEffects', () => {
  let platformServiceSpy: jasmine.SpyObj<PlatformService>;

  beforeEach(() => {
    platformServiceSpy = jasmine.createSpyObj('PlatformService', ['resume']);
  });

  // 封装创建Effect的逻辑,减少重复代码
  const createEffects = () => new PlatformEffects(platformServiceSpy);

  it('should be created', () => {
    expect(createEffects()).toBeTruthy();
  });

  describe('resume$', () => {
    it('dispatches appResumed action on resume event', (done) => {
      // Arrange:设置Spy返回预期的Observable
      platformServiceSpy.resume.and.returnValue(of(undefined));
      const effects = createEffects();

      // Act:订阅Effect输出
      effects.resume$.subscribe({
        next: (action) => {
          expect(action).toEqual(PlatformActions.appResumed());
          done();
        },
        error: done.fail
      });
    });
  });
});

为什么这有效?
每次测试都创建新的Effect实例,确保它订阅的是当前测试设置的Spy返回值,完全隔离了不同测试的状态,符合AAA模式的独立性要求。


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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.04.28 22:17:39