RxJS 大理石测试
使用大理石图测试 RxJS 代码
本指南涉及使用新的 testScheduler.run(callback)
时大理石图的用法。如果不使用 run()
帮助器,此处的某些详细信息不适用于手动使用 TestScheduler 的情况。
通过使用 TestScheduler 虚拟化时间,我们可以同步和确定性地测试异步 RxJS 代码。ASCII 大理石图为我们提供了一种直观的方式来表示 Observable 的行为。我们可以使用它们来断言特定的 Observable 的行为符合预期,以及创建可以用作模拟的冷热 Observable。
目前,TestScheduler 仅可用于测试使用计时器的代码,例如 delay / debounceTime / etc(即,它使用 AsyncScheduler 且延迟& 1)。如果代码消耗 Promise 或使用 AsapScheduler / AnimationFrameScheduler /等进行调度,则无法使用 TestScheduler 对其进行可靠的测试,而应采用更传统的方式进行测试。有关更多详细信息,请参见“ 已知问题部分。
import { TestScheduler } from 'rxjs/testing';
const testScheduler = new TestScheduler((actual, expected) => {
// asserting the two objects are equal
// e.g. using chai.
expect(actual).deep.equal(expected);
});
// This test will actually run *synchronously*
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);
});
});
API
提供给您的回调函数 testScheduler.run(callback)
由 helpers
对象调用,该对象包含用于编写测试的函数。
当执行此回调中的代码时,任何使用计时器/ AsyncScheduler 的运算符(例如,延迟,debounceTime 等)都将自动**使用 TestScheduler,以便我们拥有“虚拟时间”。您不需要像过去一样将 TestScheduler 传递给他们。
testScheduler.run(helpers => {
const { cold, hot, expectObservable, expectSubscriptions, flush } = helpers;
// use them
});
尽管 run()
完全同步执行,但回调函数内部的辅助函数却没有!这些函数调度断言,这些断言将在回调完成或显式调用时执行 flush()
。警惕 expect
在回调中调用同步断言,例如, 从所选的测试库中调用。。
hot(marbleDiagram: string, values?: object, error?: any)
-创建一个“热”的可观察对象(类似于主题),其行为就像测试开始时已经在“运行”。一个有趣的区别是,hot
大理石允许^
角色发出“零帧”位置的信号。这是开始订阅要测试的可观察对象的默认点(可以配置-参见expectObservable
下文)。cold(marbleDiagram: string, values?: object, error?: any)
-创建一个“冷”可观察的对象,其可在测试开始时开始订阅。expectObservable(actual: Observable<T>, subscriptionMarbles?: string).toBe(marbleDiagram: string, values?: object, error?: any)
-计划何时刷新TestScheduler 的断言。给出subscriptionMarbles
的参数更改订阅和退订的时间表。如果不提供该subscriptionMarbles
参数,它将在开始时进行订阅,并且永远不会退订。阅读以下有关订阅大理石图的信息。expectSubscriptions(actualSubscriptionLogs: SubscriptionLog[]).toBe(subscriptionMarbles: string)
-就像expectObservable
为 testScheduler 刷新的时间安排断言一样。双方cold()
并hot()
返回一个可观察与属性subscriptions
类型SubscriptionLog[]
。给subscriptions
作为参数传递给expectSubscriptions
断言它是否匹配subscriptionsMarbles
在给定的大理石图toBe()
。订阅大理石图与可观察大理石图略有不同。在下面阅读更多内容。flush()
-立即开始虚拟时间。很少使用,因为run()
它将在回调返回时自动为您刷新,但是在某些情况下,您可能希望刷新一次以上,否则将获得更多控制权。
大理石语法
在 TestScheduler 的上下文中,大理石图是一个包含特殊语法的字符串,表示在虚拟时间内发生的事件。时间按帧前进。任何大理石弦的第一个字符始终代表零帧或时间的开始。在testScheduler.run(callback)
frameTimeFactor 的内部设置为 1,这意味着一帧等于一虚拟毫秒。
一帧代表多少个虚拟毫秒取决于的值 TestScheduler.frameTimeFactor
。由于遗留原因,仅当您的回调中的代码正在运行时,值才 frameTimeFactor
为 1 。外部设置为 10。在以后的 RxJS 版本中可能会更改,因此始终为1。testScheduler.run(callback)
重要提示:本语法指南涉及使用new时大理石图的用法
testScheduler.run(callback)
。手动使用 TestScheduler 时,大理石图的语义不同,并且不支持某些功能,例如新的时间进度语法。
' '
空白:水平空白将被忽略,可用于帮助垂直对齐多个大理石图。'-'
帧:虚拟时间传递的1个“帧”(请参见帧的上述说明)。[0-9]+[ms|s|m]
时间进度:时间进度语法使您可以将虚拟时间提前特定的时间。它是一个数字,后跟时间单位ms
(毫秒),s
(秒)或m
(分钟),两者之间没有任何空格,例如a 10ms b
。有关更多详细信息,请参见时间进度语法。'|'
complete:成功完成一个可观察的对象。这是可观察到的生产者信号complete()
。'#'
错误:终止可观察值的错误。这是可观察到的生产者信号error()
。[a-z0-9]
例如'a'
任何字母数字字符:表示生产者信令发出的值next()
。还请考虑您可以将其映射到这样的对象或数组中:
const expected = '400ms (a-b|)';
const values = {
a: 'value emitted',
b: 'another value emitter',
};
expectObservable(someStreamForTesting)
.toBe(expected, values);
// This would work also
const expected = '400ms (0-1|)';
const values = [
'value emitted',
'another value emitted',
];
expectObservable(someStreamForTesting)
.toBe(expected, values);
'()'
同步分组:当多个事件需要同步在同一帧中时,使用括号将这些事件分组。您可以通过这种方式将下一个值,完成或错误分组。初始位置(
确定了其值的发出时间。虽然一开始可能很不直观,但是在所有值同步发出之后,将进行一些帧运算,这些帧等于组中的 ASCII 字符数,包括括号在内。例如,'(abc)'
将在同一帧中同步发出 a,b 和 c 的值,然后将虚拟时间提前 5 帧,'(abc)'.length === 5
。这样做是因为它通常可以帮助您垂直对齐大理石图,但这是实际测试中的已知痛点。了解有关已知问题的更多信息。'^'
订阅点:(仅热观测值)显示测试的可观测物将订阅到该热观测值的点。这是可观察到的“零帧”,在之前的每一帧^
都会为负。消极的时间似乎毫无意义,但实际上在某些高级情况下有必要这样做,通常涉及 ReplaySubjects。
时间进度语法
新的时间进度语法从 CSS 持续时间语法中获得启发。它是一个数字(整数或浮点数),后面紧跟一个单位;ms(毫秒),s(秒),m(分钟)。例如100ms
,1.4s
,5.25m
。
如果不是图的第一个字符,则必须在前后添加空格,以使其与一系列弹珠区分开来。例如 a 1ms b
需要空格,因为 a1msb
将被解释为['a', '1', 'm', 's', 'b']
这些字符中的每个字符都是将被原样next()的值。
注意:您可能需要从要进行的时间中减去 1 毫秒,因为字母数字大理石(代表实际的发射值)在发射后本身已经提前了 1 个虚拟帧。这可能是很不直观和令人沮丧的,但目前确实是正确的。
const input = ' -a-b-c|';
const expected = '-- 9ms a 9ms b 9ms (c|)';
/*
// Depending on your personal preferences you could also
// use frame dashes to keep vertical aligment with the input
const input = ' -a-b-c|';
const expected = '------- 4ms a 9ms b 9ms (c|)';
// or
const expected = '-----------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
,在第 10,012 帧发射 c
,然后在第 10,013 帧发射complete
。
'--a 2.5m b'
:在第 2 帧发出 a
,在第 150,003 帧发出,b
并且永不完成。
订阅弹珠
该expectSubscriptions
助手允许你断言一个 cold()
或 hot()
创建可观测是订阅/退订在正确的时间点。在 subscriptionMarbles
对参数 expectObservable
允许您的测试,以延迟订制了更高版本的虚拟时间,和/或即使观察到被测试尚未完成退订。
订阅大理石语法与常规大理石语法略有不同。
'-'
时间:经过1帧时间。[0-9]+[ms|s|m]
时间进度:时间进度语法使您可以将虚拟时间提前特定的时间。它是一个数字,后跟时间单位ms
(毫秒),s
(秒)或m
(分钟),两者之间没有任何空格,例如a 10ms b
。有关更多详细信息,请参见时间进度语法。'^'
订阅点:显示订阅发生的时间点。'!'
取消订阅点:显示取消订阅的时间点。
订购大理石图中,最多 应有一个^
点,并且最多 应有一个!
点。除此之外,该-
角色是订阅大理石图中唯一允许使用的角色。
例子
'-'
或'------'
:从未发生过订阅。
'--^--'
:订阅在经过 2 个“帧”的时间后发生,并且该订阅并未取消订阅。
'--^--!-'
:在第 2 帧发生了订阅,而在第 5 帧未订阅。
'500ms ^ 1s !'
:在第 500 帧发生了订阅,而在第 1,501 帧未订阅。
给定热源,测试多个在不同时间订阅的订户:
testScheduler.run(({ hot, expectObservable }) => {
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);
});
手动退订永远无法完成的来源:
it('should repeat forever', () => {
const testScheduler = createScheduler();
testScheduler.run(({ expectObservable }) => {
const foreverStream$ = interval(1).pipe(mapTo('a'));
// Omitting this arg may crash the test suite.
const unsub = '------ !';
expectObservable(foreverStream$, unsub).toBe('-aaaaa');
});
});
同步断言
有时,我们需要在可观察到的流完成后断言状态的变化-例如当副作用 tap
更新变量时。在使用 TestScheduler进 行 Marbles 测试之外,我们可能会认为这是造成延迟或在声明之前等待。
例如:
let eventCount = 0;
const s1 = cold('--a--b|', { a: 'x', b: 'y' });
// side effect using 'tap' updates a variable
const result = s1.pipe(tap(() => eventCount++));
expectObservable(result).toBe('--a--b|', ['x', 'y']);
// flush - run 'virtual time' to complete all outstanding hot or cold observables
flush();
expect(eventCount).toBe(2);
在上述情况下,我们需要完成可观察的流,以便我们可以测试将变量设置为正确的值。TestScheduler 在“虚拟时间”(同步)中运行,但是通常不会运行(并完成),直到 testScheduler 回调返回。flush()方法手动触发虚拟时间,以便我们在可观察值完成后测试局部变量。
已知的问题
您无法直接测试使用 Promise 或使用任何其他调度程序的 RxJS 代码(例如 AsapScheduler)
如果您有 RxJS代码使用 AsyncScheduler 以外的其他任何形式的异步调度,例如 Promises,AsapScheduler 等,则无法可靠地将大理石图用于该特定代码。这是因为那些其他的调度方法不会被虚拟化,也不会为 TestScheduler所了解。
解决方案是使用测试框架的传统异步测试方法来隔离测试该代码。具体细节取决于您选择的测试框架,但这是一个伪代码示例:
// Some RxJS code that also consumes a Promise, so TestScheduler won't be able
// to correctly virtualize and the test will always be really async
const myAsyncCode = () => from(Promise.resolve('something'));
it('has async code', done => {
myAsyncCode().subscribe(d => {
assertEqual(d, 'something');
done();
});
});
与此相关的是,即使使用 AsyncScheduler,您目前也无法断言零延迟,例如 delay(0)
说 setTimeout(work, 0)
。这样可以安排一个新的“任务”(又称为“宏任务”),因此它是异步的,但没有明确的时间间隔。
行为与外界不同 testScheduler.run(callback)
TestScheduler 从 v5 开始就存在,但实际上是旨在由维护人员测试 RxJS 本身,而不是用于常规用户应用程序中。因此,TestScheduler 的某些默认行为和功能对用户而言效果不佳(或根本不起作用)。在 V6 我们介绍了testScheduler.run(callback)
这使我们能够提供新的默认值,并在非打破方式特征的方法,但它仍然可以使用TestScheduler之外的 testScheduler.run(callback)
。重要的是要注意,如果这样做,它的行为会有一些主要差异。
- TestScheduler 帮助器方法具有更多详细名称,例如
testScheduler.createColdObservable()
而不是cold()
- 使用 AsyncScheduler 的操作员不会自动使用 testScheduler 实例,例如,延迟,debounceTime 等,因此您必须将其明确传递给他们。
- 不支持时间进度语法,例如
-a 100ms b-|
- 默认情况下,一帧是 10 个虚拟毫秒。即
TestScheduler.frameTimeFactor = 10
- 每个空格
`等于1帧,与连字符相同
-`。 - 硬的最大帧数设置为 750,即
maxFrames = 750
。750 之后,它们会被静默忽略。 - 您必须显式刷新调度程序
尽管此时 testScheduler.run(callback)
尚未正式弃用外部的 TestScheduler ,但不建议使用它,因为它可能会引起混乱。