RxJS 大理石測(cè)試

2020-09-25 16:12 更新

使用大理石圖測(cè)試 RxJS 代碼

本指南涉及使用新的 testScheduler.run(callback)時(shí)大理石圖的用法。如果不使用 run()幫助器,此處的某些詳細(xì)信息不適用于手動(dòng)使用 TestScheduler 的情況。

通過使用 TestScheduler 虛擬化時(shí)間,我們可以同步和確定性地測(cè)試異步 RxJS 代碼。ASCII 大理石圖為我們提供了一種直觀的方式來(lái)表示 Observable 的行為。我們可以使用它們來(lái)斷言特定的 Observable 的行為符合預(yù)期,以及創(chuàng)建可以用作模擬的冷熱 Observable。

目前,TestScheduler 僅可用于測(cè)試使用計(jì)時(shí)器的代碼,例如 delay / debounceTime / etc(即,它使用 AsyncScheduler 且延遲& 1)。如果代碼消耗 Promise 或使用 AsapScheduler / AnimationFrameScheduler /等進(jìn)行調(diào)度,則無(wú)法使用 TestScheduler 對(duì)其進(jìn)行可靠的測(cè)試,而應(yīng)采用更傳統(tǒng)的方式進(jìn)行測(cè)試。有關(guān)更多詳細(xì)信息,請(qǐng)參見“ 已知問題部分。

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

提供給您的回調(diào)函數(shù) testScheduler.run(callback)helpers對(duì)象調(diào)用,該對(duì)象包含用于編寫測(cè)試的函數(shù)。

當(dāng)執(zhí)行此回調(diào)中的代碼時(shí),任何使用計(jì)時(shí)器/ AsyncScheduler 的運(yùn)算符(例如,延遲,debounceTime 等)都將自動(dòng)**使用 TestScheduler,以便我們擁有“虛擬時(shí)間”。您不需要像過去一樣將 TestScheduler 傳遞給他們。

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

盡管 run()完全同步執(zhí)行,但回調(diào)函數(shù)內(nèi)部的輔助函數(shù)卻沒有!這些函數(shù)調(diào)度斷言,這些斷言將在回調(diào)完成或顯式調(diào)用時(shí)執(zhí)行 flush()。警惕 expect 在回調(diào)中調(diào)用同步斷言,例如, 從所選的測(cè)試庫(kù)中調(diào)用。。

  • hot(marbleDiagram: string, values?: object, error?: any)-創(chuàng)建一個(gè)“熱”的可觀察對(duì)象(類似于主題),其行為就像測(cè)試開始時(shí)已經(jīng)在“運(yùn)行”。一個(gè)有趣的區(qū)別是,hot 大理石允許^角色發(fā)出“零幀”位置的信號(hào)。這是開始訂閱要測(cè)試的可觀察對(duì)象的默認(rèn)點(diǎn)(可以配置-參見 expectObservable下文)。
  • cold(marbleDiagram: string, values?: object, error?: any)-創(chuàng)建一個(gè)“冷”可觀察的對(duì)象,其可在測(cè)試開始時(shí)開始訂閱。
  • expectObservable(actual: Observable<T>, subscriptionMarbles?: string).toBe(marbleDiagram: string, values?: object, error?: any)-計(jì)劃何時(shí)刷新TestScheduler 的斷言。給出 subscriptionMarbles的參數(shù)更改訂閱和退訂的時(shí)間表。如果不提供該 subscriptionMarbles參數(shù),它將在開始時(shí)進(jìn)行訂閱,并且永遠(yuǎn)不會(huì)退訂。閱讀以下有關(guān)訂閱大理石圖的信息。
  • expectSubscriptions(actualSubscriptionLogs: SubscriptionLog[]).toBe(subscriptionMarbles: string)-就像 expectObservable為 testScheduler 刷新的時(shí)間安排斷言一樣。雙方 cold()hot()返回一個(gè)可觀察與屬性 subscriptions類型 SubscriptionLog[]。給 subscriptions作為參數(shù)傳遞給 expectSubscriptions斷言它是否匹配 subscriptionsMarbles在給定的大理石圖 toBe()。訂閱大理石圖與可觀察大理石圖略有不同。在下面閱讀更多內(nèi)容。
  • flush()-立即開始虛擬時(shí)間。很少使用,因?yàn)?code>run()它將在回調(diào)返回時(shí)自動(dòng)為您刷新,但是在某些情況下,您可能希望刷新一次以上,否則將獲得更多控制權(quán)。

大理石語(yǔ)法

在 TestScheduler 的上下文中,大理石圖是一個(gè)包含特殊語(yǔ)法的字符串,表示在虛擬時(shí)間內(nèi)發(fā)生的事件。時(shí)間按前進(jìn)。任何大理石弦的第一個(gè)字符始終代表零幀或時(shí)間的開始。在testScheduler.run(callback)frameTimeFactor 的內(nèi)部設(shè)置為 1,這意味著一幀等于一虛擬毫秒。

一幀代表多少個(gè)虛擬毫秒取決于的值 TestScheduler.frameTimeFactor。由于遺留原因,當(dāng)您的回調(diào)中的代碼正在運(yùn)行時(shí),值 frameTimeFactor為 1 。外部設(shè)置為 10。在以后的 RxJS 版本中可能會(huì)更改,因此始終為1。testScheduler.run(callback)

重要提示:本語(yǔ)法指南涉及使用new時(shí)大理石圖的用法testScheduler.run(callback)。手動(dòng)使用 TestScheduler 時(shí),大理石圖的語(yǔ)義不同,并且不支持某些功能,例如新的時(shí)間進(jìn)度語(yǔ)法。

  • ' ' 空白:水平空白將被忽略,可用于幫助垂直對(duì)齊多個(gè)大理石圖。
  • '-' 幀:虛擬時(shí)間傳遞的1個(gè)“幀”(請(qǐng)參見幀的上述說(shuō)明)。
  • [0-9]+[ms|s|m]時(shí)間進(jìn)度:時(shí)間進(jìn)度語(yǔ)法使您可以將虛擬時(shí)間提前特定的時(shí)間。它是一個(gè)數(shù)字,后跟時(shí)間單位ms(毫秒),s(秒)或m(分鐘),兩者之間沒有任何空格,例如 a 10ms b。有關(guān)更多詳細(xì)信息,請(qǐng)參見時(shí)間進(jìn)度語(yǔ)法。
  • '|'complete:成功完成一個(gè)可觀察的對(duì)象。這是可觀察到的生產(chǎn)者信號(hào) complete()。
  • '#'錯(cuò)誤:終止可觀察值的錯(cuò)誤。這是可觀察到的生產(chǎn)者信號(hào) error()
  • [a-z0-9]例如'a'任何字母數(shù)字字符:表示生產(chǎn)者信令發(fā)出的值 next()。還請(qǐng)考慮您可以將其映射到這樣的對(duì)象或數(shù)組中:

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);

  • '()'同步分組:當(dāng)多個(gè)事件需要同步在同一幀中時(shí),使用括號(hào)將這些事件分組。您可以通過這種方式將下一個(gè)值,完成或錯(cuò)誤分組。初始位置(確定了其值的發(fā)出時(shí)間。雖然一開始可能很不直觀,但是在所有值同步發(fā)出之后,將進(jìn)行一些幀運(yùn)算,這些幀等于組中的 ASCII 字符數(shù),包括括號(hào)在內(nèi)。例如,'(abc)'將在同一幀中同步發(fā)出 a,b 和 c 的值,然后將虛擬時(shí)間提前 5 幀,'(abc)'.length === 5。這樣做是因?yàn)樗ǔ?梢詭椭怪睂?duì)齊大理石圖,但這是實(shí)際測(cè)試中的已知痛點(diǎn)。了解有關(guān)已知問題的更多信息。
  • '^'訂閱點(diǎn):(僅熱觀測(cè)值)顯示測(cè)試的可觀測(cè)物將訂閱到該熱觀測(cè)值的點(diǎn)。這是可觀察到的“零幀”,在之前的每一幀^都會(huì)為負(fù)。消極的時(shí)間似乎毫無(wú)意義,但實(shí)際上在某些高級(jí)情況下有必要這樣做,通常涉及 ReplaySubjects。

時(shí)間進(jìn)度語(yǔ)法

新的時(shí)間進(jìn)度語(yǔ)法從 CSS 持續(xù)時(shí)間語(yǔ)法中獲得啟發(fā)。它是一個(gè)數(shù)字(整數(shù)或浮點(diǎn)數(shù)),后面緊跟一個(gè)單位;ms(毫秒),s(秒),m(分鐘)。例如100ms,1.4s5.25m。

如果不是圖的第一個(gè)字符,則必須在前后添加空格,以使其與一系列彈珠區(qū)分開來(lái)。例如 a 1ms b需要空格,因?yàn)?a1msb將被解釋為['a', '1', 'm', 's', 'b']這些字符中的每個(gè)字符都是將被原樣next()的值。

注意:您可能需要從要進(jìn)行的時(shí)間中減去 1 毫秒,因?yàn)樽帜笖?shù)字大理石(代表實(shí)際的發(fā)射值)在發(fā)射后本身已經(jīng)提前了 1 個(gè)虛擬幀。這可能是很不直觀和令人沮喪的,但目前確實(shí)是正確的。

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(),或從不發(fā)出或完成的可觀察物

|`: 相當(dāng)于 `empty()
#`: 相當(dāng)于 `throwError()

'--a--':等待 2 個(gè)“幀”的可觀察對(duì)象,發(fā)出值 a,然后永不完成。

'--a--b--|'`:在第2幀發(fā)射`a`,在第5幀發(fā)射`b`和在第8幀上`complete
'--a--b--#'`:在第2幀發(fā)射`a`,在第5幀發(fā)射`b`和在第8幀上`error

'-a-^-b--|':在熱觀測(cè)下,在 -2 幀上發(fā)射 a,然后在第 2 幀上發(fā)射 b,在第5幀上,complete

'--(abc)-|'`:在第 2 幀上發(fā)出`a`,`b`和`c`,然后在第 8 幀上發(fā)出`complete

'-----(a|)':在第5幀發(fā)出acomplete

'a 9ms b 9s c|':在第 0 幀發(fā)射 a,在第 10 幀發(fā)射 b,在第 10,012 幀發(fā)射 c,然后在第 10,013 幀發(fā)射complete

'--a 2.5m b':在第 2 幀發(fā)出 a,在第 150,003 幀發(fā)出,b并且永不完成。

訂閱彈珠

expectSubscriptions助手允許你斷言一個(gè) cold()hot()創(chuàng)建可觀測(cè)是訂閱/退訂在正確的時(shí)間點(diǎn)。在 subscriptionMarbles對(duì)參數(shù) expectObservable允許您的測(cè)試,以延遲訂制了更高版本的虛擬時(shí)間,和/或即使觀察到被測(cè)試尚未完成退訂。

訂閱大理石語(yǔ)法與常規(guī)大理石語(yǔ)法略有不同。

  • '-' 時(shí)間:經(jīng)過1幀時(shí)間。
  • [0-9]+[ms|s|m]時(shí)間進(jìn)度:時(shí)間進(jìn)度語(yǔ)法使您可以將虛擬時(shí)間提前特定的時(shí)間。它是一個(gè)數(shù)字,后跟時(shí)間單位ms(毫秒),s(秒)或m(分鐘),兩者之間沒有任何空格,例如 a 10ms b。有關(guān)更多詳細(xì)信息,請(qǐng)參見時(shí)間進(jìn)度語(yǔ)法。
  • '^' 訂閱點(diǎn):顯示訂閱發(fā)生的時(shí)間點(diǎn)。
  • '!' 取消訂閱點(diǎn):顯示取消訂閱的時(shí)間點(diǎn)。

訂購(gòu)大理石圖中,最多 應(yīng)有一個(gè)^點(diǎn),并且最多 應(yīng)有一個(gè)!點(diǎn)。除此之外,該-角色是訂閱大理石圖中唯一允許使用的角色。

例子

'-''------':從未發(fā)生過訂閱。

'--^--':訂閱在經(jīng)過 2 個(gè)“幀”的時(shí)間后發(fā)生,并且該訂閱并未取消訂閱。

'--^--!-':在第 2 幀發(fā)生了訂閱,而在第 5 幀未訂閱。

'500ms ^ 1s !':在第 500 幀發(fā)生了訂閱,而在第 1,501 幀未訂閱。

給定熱源,測(cè)試多個(gè)在不同時(shí)間訂閱的訂戶:

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);
});

手動(dòng)退訂永遠(yuǎn)無(wú)法完成的來(lái)源:

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');
  });
});

同步斷言

有時(shí),我們需要在可觀察到的流完成斷言狀態(tài)的變化-例如當(dāng)副作用 tap 更新變量時(shí)。在使用 TestScheduler進(jìn) 行 Marbles 測(cè)試之外,我們可能會(huì)認(rèn)為這是造成延遲或在聲明之前等待。

例如:

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);

在上述情況下,我們需要完成可觀察的流,以便我們可以測(cè)試將變量設(shè)置為正確的值。TestScheduler 在“虛擬時(shí)間”(同步)中運(yùn)行,但是通常不會(huì)運(yùn)行(并完成),直到 testScheduler 回調(diào)返回。flush()方法手動(dòng)觸發(fā)虛擬時(shí)間,以便我們?cè)诳捎^察值完成后測(cè)試局部變量。

已知的問題

您無(wú)法直接測(cè)試使用 Promise 或使用任何其他調(diào)度程序的 RxJS 代碼(例如 AsapScheduler)

如果您有 RxJS代碼使用 AsyncScheduler 以外的其他任何形式的異步調(diào)度,例如 Promises,AsapScheduler 等,則無(wú)法可靠地將大理石圖用于該特定代碼。這是因?yàn)槟切┢渌恼{(diào)度方法不會(huì)被虛擬化,也不會(huì)為 TestScheduler所了解。

解決方案是使用測(cè)試框架的傳統(tǒng)異步測(cè)試方法來(lái)隔離測(cè)試該代碼。具體細(xì)節(jié)取決于您選擇的測(cè)試框架,但這是一個(gè)偽代碼示例:

// 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();
  });
});

與此相關(guān)的是,即使使用 AsyncScheduler,您目前也無(wú)法斷言零延遲,例如 delay(0)說(shuō) setTimeout(work, 0)。這樣可以安排一個(gè)新的“任務(wù)”(又稱為“宏任務(wù)”),因此它是異步的,但沒有明確的時(shí)間間隔。

行為與外界不同 testScheduler.run(callback)

TestScheduler 從 v5 開始就存在,但實(shí)際上是旨在由維護(hù)人員測(cè)試 RxJS 本身,而不是用于常規(guī)用戶應(yīng)用程序中。因此,TestScheduler 的某些默認(rèn)行為和功能對(duì)用戶而言效果不佳(或根本不起作用)。在 V6 我們介紹了testScheduler.run(callback)這使我們能夠提供新的默認(rèn)值,并在非打破方式特征的方法,但它仍然可以使用TestScheduler之外testScheduler.run(callback)。重要的是要注意,如果這樣做,它的行為會(huì)有一些主要差異。

  • TestScheduler 幫助器方法具有更多詳細(xì)名稱,例如 testScheduler.createColdObservable()而不是cold()
  • 使用 AsyncScheduler 的操作員不會(huì)自動(dòng)使用 testScheduler 實(shí)例,例如,延遲,debounceTime 等,因此您必須將其明確傳遞給他們。
  • 不支持時(shí)間進(jìn)度語(yǔ)法,例如 -a 100ms b-|
  • 默認(rèn)情況下,一幀是 10 個(gè)虛擬毫秒。即 TestScheduler.frameTimeFactor = 10
  • 每個(gè)空格`等于1幀,與連字符相同-`。
  • 硬的最大幀數(shù)設(shè)置為 750,即 maxFrames = 750。750 之后,它們會(huì)被靜默忽略。
  • 您必須顯式刷新調(diào)度程序

盡管此時(shí) testScheduler.run(callback)尚未正式棄用外部的 TestScheduler ,但不建議使用它,因?yàn)樗赡軙?huì)引起混亂。

以上內(nèi)容是否對(duì)您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號(hào)
微信公眾號(hào)

編程獅公眾號(hào)