RxJS Marble测试使用fromPromise失败,替换为Observable.of则正常(RxJS5)
我之前也碰到过一模一样的问题!在RxJS 5 + redux-observable 0.18的Marble测试里,fromPromise和TestScheduler配合确实容易踩坑,尤其是刚接触Marble测试的时候。咱们先搞清楚问题根源,再一步步解决:
问题根源
TestScheduler是同步执行所有调度任务的,但原生Promise的resolve/reject属于JavaScript的异步微任务,默认情况下TestScheduler不会等待这些微任务完成就会结束flush()流程,导致fromPromise发射的值根本没被测试捕获到。而Observable.of是同步发射值的,自然能被TestScheduler正常处理。
解决方案&排查思路
1. 用TestScheduler的cold/hot替代原生fromPromise(最推荐)
这是最稳妥的方案——把异步Promise的逻辑转换成Marble序列,让TestScheduler完全控制时序。比如:
假设你的原epic是这样的:
const MY_ACTION = 'MY_ACTION'; const myAction = () => ({ type: MY_ACTION }); const fetchSuccessAction = (data) => ({ type: 'FETCH_SUCCESS', payload: data }); const myEpic = (action$) => action$.ofType(MY_ACTION) .mergeMap(() => Observable.fromPromise(fetch('/api/data'))) // 这里用了fromPromise .map(data => fetchSuccessAction(data));
测试时,不要用真实的Promise,而是用cold来模拟异步返回:
import { TestScheduler } from 'rxjs/testing'; // 自定义深比较函数,redux-observable测试常用 const assertDeepEqual = (actual, expected) => expect(actual).toEqual(expected); it('should emit success action after async fetch', () => { const testScheduler = new TestScheduler(assertDeepEqual); // 模拟action流:10帧后发射MY_ACTION(用'-a'表示) const action$ = testScheduler.createColdObservable('-a', { a: myAction() }); // 模拟fetch的异步返回:20帧后返回数据'foo'(用'--(a|)'表示) const mockFetch = () => testScheduler.createColdObservable('--(a|)', { a: 'foo' }); // 修改epic支持依赖注入(redux-observable推荐的最佳实践) const testEpic = (action$) => action$.ofType(MY_ACTION) .mergeMap(() => mockFetch()) // 替换fromPromise为cold Observable .map(data => fetchSuccessAction(data)); // 预期结果:30帧后发射成功action(10+20,用'---b'表示) const expected = testScheduler.createColdObservable('---b', { b: fetchSuccessAction('foo') }); testScheduler.expectObservable(testEpic(action$)).toBe(expected); testScheduler.flush(); });
2. 让fromPromise使用TestScheduler调度器
如果你一定要保留fromPromise(比如要测试真实的Promise逻辑),可以让它使用TestScheduler的调度器来执行,这样Promise的微任务会被TestScheduler同步处理:
修改epic,支持注入调度器:
const myEpic = (action$, store, { scheduler = null }) => action$.ofType(MY_ACTION) .mergeMap(() => Observable.fromPromise(fetch('/api/data')) .subscribeOn(scheduler) // 用传入的调度器执行异步逻辑 ) .map(data => fetchSuccessAction(data));
测试时传入TestScheduler:
it('should work with fromPromise and TestScheduler', () => { const testScheduler = new TestScheduler(assertDeepEqual); const action$ = testScheduler.createColdObservable('-a', { a: myAction() }); // 模拟Promise返回 const mockFetch = () => Promise.resolve('foo'); const output$ = myEpic(action$, null, { scheduler: testScheduler, fetch: mockFetch }); // 注意:Promise至少会有一个微任务延迟,所以时序用'--b'而不是'-b' const expected = testScheduler.createColdObservable('--b', { b: fetchSuccessAction('foo') }); testScheduler.expectObservable(output$).toBe(expected); testScheduler.flush(); });
3. 快速排查小技巧
- 先把
fromPromise换成Observable.of('test-data'),如果测试通过,说明核心epic逻辑没问题,问题肯定出在异步时序上。 - 检查Marble字符串的时序是否匹配:比如
fromPromise的异步延迟至少要占1个时间帧(用-表示),不要写-(a|),要写--(a|)。 - 确保测试中没有其他原生异步操作(比如
setTimeout、真实的API请求),这些都会脱离TestScheduler的控制,导致测试失败。
内容的提问来源于stack exchange,提问作者Robert Lukens
相关产品推荐
相关产品推荐

