编写弹珠测试 | writing-marble-tests

编写弹珠测试

“弹珠测试”是使用一种叫做 TestScheduler 的专用的虚拟调度器 (VirtualScheduler) 的测试。它们可以使我们以同步且可靠的方式来测试异步操作。“弹珠符号”是源自许多人的教程与文档,例如,@jhusain、@headinthebox、@mattpodwysocki 和 @staltz 。实际上,是由 @staltz 首先提出建议将其作为创建单元测试的 DSL (Domain Specific Language 领域专用语言),并且它已经经过改造并被采纳。

基础方法

单元测试添加了一些辅助方法,以使创建测试更容易。

  • hot(marbles: string, values?: object, error?: any) - 创建一个“热的” Observable (Subject),它将在测试开始时表现得 好像已经在“运行中”了。一个有趣的不同点是 hot 弹珠允许使用^字符来标志“零帧”所在处。这正是 Observables 订阅的起始点, 同时也是测试的起始点。

hot 和 cold 的默认行为是符合人类认知的

hotcold 方法中,弹珠图中指定的值的字符都会作为字符串发出,除非将 values 参数传给了方法。因此:

hot('--a--b') 会发出 "a""b" ,但

hot('--a--b', { a: 1, b: 2 }) 会发出 12

同样的,未指明的错误就只发出默认字符串 "error",所以:

hot('---#') 会发出错误 "error" , 但

hot('---#', null, new SpecialError('test')) 会发出 new SpecialError('test')

弹珠语法

弹珠语法是用字符串表示随“时间”流逝而发生的事件。任何弹珠字符串的首字符永远都表示“零帧”。“帧”是有点类似于虚拟毫秒的概念。

  • "-" 时间: 10“帧”的时间段。

示例

'-''------': 相当于 Observable.never(),或一个从不发出值或完成的 Observable

|: 相当于 Observable.empty()

#: 相当于 Observable.throw()

'--a--': 此 Observable 等待20“帧”后发出值 a ,然后永远不发出 complete

'--a--b--|: 在20帧处发出 a,在50帧处发出 b,然后在80帧处发出 complete

'--a--b--#: 在20帧处发出 a,在50帧处发出 b,然后在80帧处发出 error

'-a-^-b--|: 这是个热的 Observable ,在-20帧处发出 a,然后在20帧处发出 b ,在50帧出发出 complete

'--(abc)-|': 在20帧处发出 abc,然后在80帧处发出 complete

'-----(a|)': 在50帧处发出 acomplete

Subscription 的弹珠语法

Subscription 的弹珠语法与常见的弹珠语法略有不同。它表示随“时间”流逝而发生的订阅取消订阅的时间点。在此类图中不应该出现其他类型的事件。

  • "-" 时间: 10“帧”的时间段。

在 Subscription 的弹珠语法中 ^! 时间点最多只有一个。 除此之外,- 字符是唯一允许出现的字符。

示例

'-''------': 没有订阅发生。

'--^--': 在20帧处发生了订阅,并且订阅没有被取消。

'--^--!-: 在20帧处发生了订阅,在50帧处订阅被取消了

测试剖析

一个基础的测试看起来应该是下面这样的:

var e1 = hot('----a--^--b-------c--|' var e2 = hot( '---d-^--e---------f-----|' var expected = '---(be)----c-f-----|'; expectObservable(e1.merge(e2)).toBe(expected

  • hot observables 的 ^ 字符应该永远是对齐的。

使用指定值的测试用例:

var values = { a: 1, b: 2, c: 3, d: 4, x: 1 + 3, // a + c y: 2 + 4, // b + d } var e1 = hot('---a---b---|', values var e2 = hot('-----c---d---|', values var expected = '-----x---y---|'; expectObservable(e1.zip(e2, function(x, y) { return x + y; })) .toBe(expected, values

  • 使用同一个散列表来查找所有的值,这可以确保多次使用的同一个字符有着同样的值。

使用 subscription 断言的测试用例:

var x = cold( '--a---b---c--|' var xsubs = '------^-------!'; var y = cold( '---d--e---f---|' var ysubs = '--------------^-------------!'; var e1 = hot( '------x-------y------|', { x: x, y: y } var expected = '--------a---b----d--e---f---|'; expectObservable(e1.switch()).toBe(expected expectSubscriptions(x.subscriptions).toBe(xsubs expectSubscriptions(y.subscriptions).toBe(ysubs

  • xsubs 图和 ysubs 图的开头与 expected 图对齐。

在大多数测试中,是没有必要测试订阅时间点和取消订阅时间点的,它们要不就非常明显,要不就在 expected 图中有所暗示。在这些情况下是不需要编写 subscription 断言的。在有内部 subscriptions 或冷的 observables 有多个订阅者的测试用例中,这些 subscription 断言还是有用的。

基于测试生成 PNG 弹珠图

通常,Jasmine 中的测试用例都是这样写的:it('should do something', function () { /* ... */ }) 。要想时测试用例可以用来生成 PNG 弹珠图,你必须使用 asDiagram(label) 函数,像这样:

it.asDiagram(operatorLabel)('should do something', function () { // ... }

举例来说,对于 zip 操作符,我们可以这样写:

it.asDiagram('zip')('should zip by concatenating', function () { var e1 = hot('---a---b---|' var e2 = hot('-----c---d---|' var expected = '-----x---y---|'; var values = { x: 'ac', y: 'bd' }; var result = e1.zip(e2, function(x, y) { return String(x) + String(y } expectObservable(result).toBe(expected, values }

然后当运行 npm run tests2png 时,这个测试用例会解析并且在 img/ 文件夹下创建一个 PNG 文件 zip.png (文件名取决于 ${operatorLabel}.png)。