0%

RxJS 弹珠图以及测试弹珠图

RxJS的弹珠图看到很多,但是不知道具体的语法是啥,还是一脸懵逼的状况,所以在回顾RxJS的操作符之前,先来学习下弹珠图的语法。

需要注意的是,我这里直接使用了angular自带的JasmineKarama来做测试,代码是写在.spec.ts文件里面的。

为了运行单组件里面的测试代码,我们只需要运行ng test --include youComponentPath即可。

我这里是边看文章边翻译的,可能也不太准确,如果介意的话可以直接翻到文章底部点击原文的链接即可。

We can test our asynchronous[异步] RxJS code synchronously[同步] and deterministically[确定性地] by virtualizing time using the TestScheduler.
通过TestScheduler提供的虚拟时间,我们可以同步和确定性的测试异步RxJS代码。

ASCII marble diagrams provide a visual way for us to represent[表示] the behavior of an Observable.
ASCII弹珠图为我们提供了一种直观的方式来表示Observable的行为。

We can use them to assert that a particular Observable behaves as expected, as well as to create hot and cold Observables we can use as mocks.
我们可以使用它们来断言特定的Observable行为符合预期,以及创建可以用作模拟的冷热Observable。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import {TestScheduler} from "rxjs/testing";
import {throttleTime} from "rxjs/operators";
const testScheduler = new TestScheduler((actual, expected) => {
expect(actual).toEqual(expected)
})

it('should create', () => {
expect(component).toBeTruthy();
});

it('generate the stream correctly', () => {
testScheduler.run(helpers => {
const {cold, expectObservable, expectSubscriptions} = helpers;
const e1 = cold('-a--b--c---|')
const subs = '^--------------------!'
const expected ='-a-----c---|'
expectObservable(e1.pipe(throttleTime(3, testScheduler))).toBe(expected)
expectSubscriptions(e1.subscriptions).toBe(subs);
})
})

运行测试后可以发现,2 specs, 1failure。是因为第二个和期望对不上。

The callback function you provide to testScheduler.run(callback) is called with helpers object that contains[包含] functions you’ll use to write you tests.
提供给testScheduler.run(callback)的回调函数由helpers对象调用,该对象包含将用于编写测试的函数。

When the code inside this callback is being executed, any operator that uses timers/AsyncScheduler(like delay, debounceTime, etc) will automatically use the TestScheduler instead, so that we have “virtual time”. You do not need to pass the TestScheduler to them.
当执行回调函数中的代码时,任何使用计时器或AsyncScheduler的运算符(例如delay/debounceTime等)将自动使用TestScheduler,以使我们拥有“虚拟时间”。不需要将TestScheduler传递给它们。

1
2
3
4
testScheduler.run(helpers => {
const {cold, hot, expectObservable, expectSubscriptions, flush} = helps;
// use them
})

Although run() executes entirely synchronously, the helper functions inside you callback function do not!
尽管run()完全同步执行,但回调函数内部的辅助函数却不是这样。

These functions schedule assertions that will execute either when you callback completes or when you explicitly call fulsh().
这些函数调度断言将在回调完成或者显示调用flush()时执行。

Be wary of calling synchronous assertions, for example expect from you testing library of choice, from within the callback.
注意在回调函数中调用同步断言,例如,从回调函数中,从你选择的测试库中设定期望。

  • hot(marbleDiagram: string, values?: object, error?: any) 创建一个hot observable。其行为就想测试开始时已经在运行。一个有趣的区别是,热弹珠允许使用^字符表示“零帧”的位置,这是对要测试的observable进行subscrible的默认点(可以使用expecteObservable来进行配置)。
  • cold(marbleDiagram: string, values?: object, error?: any) 创建一个cold observable。当测试开始时,谁订阅就允许。
  • expectObservable(actual: Observable<T>, subscriptionMarble?: string).toBe(marbleDiagram: string; value?: object, error?: any) 为TestScheduler刷新时安排断言。将subscriptionMarbles作为参数来更改订阅和取消订阅的时间表。如果不提供subscriptionMarble参数,它将在开始时订阅,并且永远不会取消订阅。
  • expectSubscriptions(actualSubscriptionLogs: SubscriptionLog[]).toBe(subscriptionMarbles: string) 就像expectObservable计划在testScheduler刷新时声明断言。cold()hot()都返回一个带有SubscriptionLog[]类型的属性订阅的可观察对象。将订阅作为参数传递给expectSubscriptions,以断言它是否与toBe()中给定的subscriptionsMarbles弹珠图是否匹配。
  • flush() 立即开始虚拟时间。很少使用,因为run()会在你的回调返回时自动刷星,但是某些情况下,可能希望刷新一次以上。

Marble Syntax 弹珠图的语法

In the context of TestScheduler, a marble diagram is a string containing special syntax representing events happening over virtual time. Time progresses by frames.
在TestScheduler的上下文中,弹珠图是一个字符串,其中包含表示虚拟时间发生的时间的特殊语法。时间按帧进行。

The first of character of any marble string always represents the zero frame, or the start of time.
弹珠图的第一个字符代表零帧或时间的开始。

Inside of testScheduler.run(callback) the frameTimeFactor is set to 1, which means on frame is equal to one virtual millisecond.
testScheduler.run(callback)内部,frameTimeFactor设置为1,这意味着一帧等于一个虚拟毫秒。

How many virtual milliseconds one frame represents depends on the value of TestSchelduer.frameTimeFactor.
一帧表示多少虚拟毫秒取决于TestScheduler.frameTimeFactor的值。

For legacy reasons the value of frameTimeFactor is 1 only when your code inside the testScheduler.run(callback) callback is runing. Outside of it, it’s set to 10. This will likely change in a furture version of RxJS so that is always 1.
由于历史原因,仅当运行testScheduler.run(callback)回调中的代码时,frameTimeFactor的值才为1。外部设置为10。在以后的RxJS版本中可能会更改,因此始终为1.

  • ' ' 空白符:水平防线的空白符会被忽略的,会被用来辅助对齐多行弹珠图。
  • - 短横线代表一帧,传递一帧虚拟时间。
  • [0-9]+[ms|s|m] 时间进度,时间进度语法使我们可以将虚拟时间进行特定量的调整。它是一个数字后面跟上表示毫秒或秒或分钟的时间单位。两者之间没有空格。
  • | 竖线符号表示完成一个可观察对象。这是可观察到生产者发出的complete信号。
  • # 表示错误,错误可以终止可观察对象。这是可以观察到生产者发出的error信号。
  • [a-z0-9] 表示生产者通过信号next发送的值。可以将其映射到数组或对象。
  • () 同步分组。当多个事件需要同步在同一帧中时,可以使用括号将这些事件分组。可以通过这种方式将下一个值(next)、完成(complete)、错误(error)以组的形式发出。
  • ^ 订阅点。(仅观测值)显示测试的Observable将被订阅到该热观测值的点。这是可观测到的“零帧”。^之前的每个帧都是负值。消极的时间似乎毫无意义,但实际上在某些高级情况下有必要这样做。通常涉及到ReplaySubjects.

Time progression syntax 时间进度语法

The new time progression syntax takes inspiration from the CSS duration syntax. It’s a number(int or flat) immediately followed by a unit; ms(millisecond), s(seconds), m(minutes).
新的时间进度语法从CSS的持续时间语法中汲取了灵感。它是一个数字(整数或浮点数),后面紧跟一个单位:毫秒、秒、分钟。

When it’s not the first character of the diagram it must be padded a space before/after to disambiguate it from a series of marbles. eg. a 1ms b needs the spaces because a1mb will be interpreted as 'a'. '1'. 'm'. 's'. 'b' where each of these characters is a value that vill be next()’d as-is.
如果不是弹珠图的第一个字符,那么必须在时间的前后添加空格以消除歧义,例如:a 1ms b,否则会将a1msb中的每个每个字符当做next的值发射出去。

需要注意的是,需要在发出值后,从要进行的时间中减去1毫秒,因为在在发出值后已经提前了一个虚拟帧。例如:

1
2
3
4
5
6
7
const {cold, expectObservable, expectSubscriptions} = helpers;
const input = '-a-b-c|'
const expected = '-- 9ms a 9ms b 9ms (c|)'
const result = cold(input).pipe(
concatMap(d => of(d).pipe(delay(10)))
)
expectObservable(result).toBe(expected)

这样测试的结果是正确的。

一些弹珠图的例子:

  • ------- 相当于never()。一个生产者从未发出值或者也从未结束。
  • | 相当于empty()
  • # 相当于throwError()
  • --a-- 生产者第2帧后发出a,从未结束。
  • --a--b--| 生产者第2帧发出a,第5帧发出b,在第8帧complete
  • --a--b--# 生产者第2帧发出a,第5帧发出b,第8帧error
  • -a-^-b--| 在一个热生产者中,第-2帧发出a,第2帧发出b,第5帧complete
  • --(abc)-| 生产者第2帧发出a、b、c三个值,第8帧complete
  • -----(a|) 生产者第5帧发出a和complete
  • a 9ms b 9s c| 生产者第0帧发出a,第10帧发出b,第9012帧发出c,在第9013帧complete

Subscription Marbles 订阅的弹珠图

The expectSubscriptions helper allows you to assert that a cold() or hot() Observable you created was subscribed/unsubscribed to at the correct point in time. The subscriptionMarbles parameter to expectObservable allows you test to defer subscription to a later virtual time, and/or unsubscribe even if the observable being tested has not yet completed.
expectSubscriptions运行对创建的hot()cold()生产者断言,在正确的时间点订阅或取消订阅。预期生产者的subscriptionMarbles参数允许你的测试将订阅推迟到以后的虚拟时间。即使正在测试的生产者尚未完成也可以取消订阅。

订阅的弹珠图与常规的弹珠图略有不同:

  • - 经过一帧时间。
  • [0-9]+[ms|s|m] 时间进度,和正常的弹珠图一样。
  • ^ 订阅点。
  • ! 取消订阅的时间点。

The should be at most one ^ point in a subscription marble diagram, and at most one ! point. Other than that, the - character is the only one allowed in a subscription marble diagram.
订阅的弹珠图中,最多应有一个^,并且最多应有一个!。除此之外,-字符是订阅弹珠图中唯一允许的一个字符。

订阅弹珠图的示例:

  • ------ 表示从未发生过订阅。
  • --^-- 表示从第2帧开始订阅,并且没有取消订阅。
  • --^--!- 表示第2帧开始订阅,第5帧取消订阅。
  • 500ms ^ 1s ! 表示第500帧开始订阅,第15001帧取消订阅。

给定热源,测试在不同时间订阅的情况:

1
2
3
4
5
6
7
8
const {cold, hot, expectObservable, expectSubscriptions} = helpers;
const source = hot('--a--a--a--a--a--a--a--');
const sub1 = '--^-----------!';
const sub2 = '---------^--------!';
const expect1 = '--a--a--a--a--';
const expect2 = '-----------a--a--a-';
expectObservable(source, sub1).toBe(expect1)
expectObservable(source, sub2).toBe(expect2)

主要了解了弹珠图的语法,和常见的例子,这样后续就可以用弹珠图的语法来表示RxJS的过程了。

我在调试过程中出现了Error: The code should be running in the fakeAsync zone to call this function的问题,查了下是需要在写It的时候加上FakeAsync,具体用法可以参考https://www.joshmorony.com/testing-asynchronous-code-with-fakeasync-in-angular/

原文链接:https://rxjs-dev.firebaseapp.com/guide/testing/marble-testing#time-progression-syntax

参考文章:
基于Jasmine和Karma的单元测试基础教程
RxJS Testing Marble Testing

码字辛苦,打赏个咖啡☕️可好?💘