使用jasmine-marbles测试Promise转Observable的NGRX Effects问题
这是个非常典型的虚拟时间测试与原生异步任务兼容性问题,我来帮你把底层逻辑和解决方案讲清楚:
问题根源:Jasmine-Marbles的虚拟时间 vs Promise的微任务
Jasmine-Marbles(本质是RxJS的TestScheduler)的核心是用虚拟时间同步执行Observable流——它会把所有Observable的事件调度都模拟在一个虚拟时间线上,不会真的等待实际的时间流逝,这样测试可以瞬间完成,且完全可控。
但Promise的异步回调是运行在浏览器/Node的微任务队列里的,这个队列的执行时机是在当前同步代码执行完之后,而Marble测试的断言是在虚拟时间推进完成后立刻执行的。当你在Effect里用from(this.backend.get(...))把Promise转成Observable时:
- Marble测试启动,推进虚拟时间到事件应该发生的点
- 此时Promise的resolve还没进入微任务队列执行,Observable还没发射任何值
- 测试立刻断言,发现Effect的流是空的,自然报错
而当你把Mock改成返回of({items: []})时,这个Observable是同步发射值的,完全在TestScheduler的虚拟时间控制范围内,所以Marble测试能准确捕获到发射的事件,测试通过。
解决方案:让Promise纳入虚拟时间控制
有几种可行的方案,你可以根据场景选择:
方案1:在Mock中用TestScheduler处理Promise
直接在Mock里把Promise转成由测试调度器控制的Observable,这样Promise的resolve会被纳入虚拟时间线:
import { TestScheduler } from 'rxjs/testing'; let testScheduler: TestScheduler; beforeEach(() => { testScheduler = new TestScheduler((actual, expected) => { expect(actual).toEqual(expected); }); const backend = { get: () => testScheduler.run(() => from(Promise.resolve({ items: [] }))) }; TestBed.configureTestingModule({ providers: [{ provide: BackendServiceClient, useValue: backend }] }); });
方案2:在Effect中注入调度器(更规范的做法)
在Effect里显式指定调度器来处理Promise转Observable的逻辑,测试时注入TestScheduler:
修改Effect代码:
import { SchedulerLike, asyncScheduler, ASYNC_SCHEDULER } from 'rxjs'; @Injectable() export class MissionsEffects { @Effect() loadMissions$ = this.dataPersistence.fetch( MissionsActionTypes.LoadMissions, { run: (action: LoadMissions, state: MissionsPartialState) => { // 用注入的调度器处理Promise return from(this.backend.get(new GetMissions()), this.scheduler).pipe( map(r => new MissionsLoaded(r.items)) ); }, onError: (action: LoadMissions, error) => { console.error('Error', error); return new MissionsLoadError(error); } } ); constructor( private dataPersistence: DataPersistence<MissionsPartialState>, private backend: BackendClientService, // 注入调度器,默认用asyncScheduler @Inject(ASYNC_SCHEDULER) private scheduler: SchedulerLike = asyncScheduler ) {} }
测试时注入TestScheduler:
beforeEach(() => { testScheduler = new TestScheduler((actual, expected) => { expect(actual).toEqual(expected); }); TestBed.configureTestingModule({ providers: [ { provide: BackendServiceClient, useValue: { get: () => Promise.resolve({ items: [] }) } }, { provide: ASYNC_SCHEDULER, useValue: testScheduler } ] }); });
方案3:直接Mock成同步Observable(最简单)
如果你只是想测试Effect对后端返回值的处理逻辑,而非Promise的异步行为,直接把Mock改成返回of(...)就完全足够——这是测试中最常用的做法,因为我们不需要在Effect测试中验证Promise的异步特性,那是后端服务本身该测的内容。
总结
核心矛盾就是Promise的微任务脱离了Marble测试的虚拟时间控制,导致断言时异步结果还未生效。只要把Promise的执行纳入TestScheduler的虚拟时间线,或者换成同步Observable来Mock,就能解决问题。
内容的提问来源于stack exchange,提问作者Ludovic C

