编写弹珠测试 | 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 的默认行为是符合人类认知的
在 hot
和 cold
方法中,弹珠图中指定的值的字符都会作为字符串发出,除非将 values
参数传给了方法。因此:
hot('--a--b')
会发出 "a"
和 "b"
,但
hot('--a--b', { a: 1, b: 2 })
会发出 1
和 2
。
同样的,未指明的错误就只发出默认字符串 "error"
,所以:
hot('---#')
会发出错误 "error"
, 但
hot('---#', null, new SpecialError('test'))
会发出 new SpecialError('test')
。
弹珠语法
弹珠语法是用字符串表示随“时间”流逝而发生的事件。任何弹珠字符串的首字符永远都表示“零帧”。“帧”是有点类似于虚拟毫秒的概念。
"-"
时间: 10“帧”的时间段。
示例
'-'
或 '------'
: 相当于 Observable.never()
,或一个从不发出值或完成的 Observable
|
: 相当于 Observable.empty()
#
: 相当于 Observable.throw()
'--a--'
: 此 Observa
ble 等待20“帧”后发出值 a
,然后永远不发出 complete
'--a--b--|
: 在20帧处发出 a
,在50帧处发出 b
,然后在80帧处发出 complete
'--a--b--#
: 在20帧处发出 a
,在50帧处发出 b
,然后在80帧处发出 error
'-a-^-b--|
: 这是个热的 Ob
serva
b
le ,在-20帧处发出 a
,然后在20帧处发出 b
,在50帧出发出 complete
'--(abc)-|'
: 在20帧处发出 a
、b
和 c
,然后在80帧处发出 complete
'-----(a|)'
: 在50帧处发出 a
和 complete
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
)。