如何测试一帧内发射多值的RxJS Observable及帧匹配问题
背景代码
业务实现代码
import {merge, Observable} from "rxjs"; export function springLatch(handle$: Observable<boolean>, closedByDefault: boolean): Observable<boolean> { const latch$ = new Observable<boolean>((subscriber) => { if (closedByDefault) { subscriber.next(false); } else { subscriber.next(true); } handle$.subscribe({ next(value: boolean) { if (closedByDefault && value) { subscriber.next(value); } else { subscriber.next(value); } } }) }); return merge(handle$, latch$); }
单元测试代码
describe('SpringLatchTest', () => { let testScheduler: TestScheduler; beforeEach(() => { testScheduler = new TestScheduler((actual, expected) => { expect(actual).toBe(expected); }); }); it('should do something', () => { testScheduler.run(({cold, expectObservable}) => { // 组装 const handle$ = cold('-t-f', {t: true, f: false}); // 执行 const actual: Observable<boolean> = springLatch(handle$, true); // 断言 expectObservable(actual).toBe('fttff', {t: true, f: false}); }); }); });
依赖版本
"jest": "^29.5.0", "rxjs": "^7.8.0"
问题解答
问题1:如何避免单元测试因实际Observable与预期Observable的大理石图帧不匹配而失败?
可以从以下几点解决:
精准匹配初始发射的帧位置
你的springLatch中,latch$会在订阅时立即发射初始值(对应帧0),merge会同时订阅handle$和latch$。handle$的'-t-f'表示帧0无发射、帧1发射true、帧3发射false;同时latch$会订阅handle$并重复发射所有值,所以合并后的实际发射是:帧0发射false,帧1发射true(handle$)+true(latch$),帧3发射false(handle$)+false(latch$)。你当前的预期'fttff'不符合这个时序,需要调整为'f-tt-ff'这类精准匹配的大理石图。吃透TestScheduler的帧规则
大理石图中每个字符对应一个虚拟帧:-代表空帧(无发射),字母/符号代表对应帧的发射动作。必须确保预期的每个帧位置都和实际Observable的发射时刻严格对齐。先修正业务逻辑冗余
你的latch$中,if (closedByDefault && value)分支和else分支的逻辑完全一致,都会发射value,这会导致handle$的每个值被重复发射两次。如果这不是业务预期,先修正逻辑,否则必然出现帧不匹配的问题。使用精确断言
expectObservable(actual).toBe()的大理石图和值映射必须完全对应实际发射的时序与内容,不能凭主观猜测,要根据业务逻辑推导每一次发射的帧位置和对应值。
问题2:值的发射与帧是否存在一一对应的关系?
在RxJS的TestScheduler中,值的发射与帧是严格一一对应的:
- 每个帧代表一个虚拟时间单位,Observable的每一次值发射(包括完成、错误信号)都会绑定到特定的帧位置。
- 比如
'-t-f'中,t对应帧1的发射,f对应帧3的发射;反过来,帧1的发射动作对应t的值,帧3的发射动作对应f的值。 - 空帧(
-)表示该时间单位内无任何发射,只有带值标识的位置才对应一次发射。
如果同一帧有多个值发射(比如merge后同一帧出现两次发射),大理石图需要用括号包裹,比如'(ft)'表示帧0同时发射f和t,此时两个值对应同一个帧,但每个值仍属于该帧的发射动作。
内容的提问来源于stack exchange,提问作者Byebye

