RxJS 大理石测试

2020-09-25 16:12 更新

使用大理石图测试 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(分钟)。例如100ms1.4s5.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帧发出acomplete

'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 ,但不建议使用它,因为它可能会引起混乱。

以上内容是否对您有帮助:
在线笔记
App下载
App下载

扫描二维码

下载编程狮App

公众号
微信公众号

编程狮公众号