如何优雅测试使用依赖注入服务事件的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

