RxJS 大理石測試

2020-09-25 16:12 更新

使用大理石圖測試 RxJS 代碼

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

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

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

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對象調(diào)用,該對象包含用于編寫測試的函數(shù)。

當執(zhí)行此回調(diào)中的代碼時,任何使用計時器/ AsyncScheduler 的運算符(例如,延遲,debounceTime 等)都將自動**使用 TestScheduler,以便我們擁有“虛擬時間”。您不需要像過去一樣將 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)用時執(zhí)行 flush()。警惕 expect 在回調(diào)中調(diào)用同步斷言,例如, 從所選的測試庫中調(diào)用。。

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

大理石語法

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

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

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

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

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

時間進度語法

新的時間進度語法從 CSS 持續(xù)時間語法中獲得啟發(fā)。它是一個數(shù)字(整數(shù)或浮點數(shù)),后面緊跟一個單位;ms(毫秒),s(秒),m(分鐘)。例如100ms,1.4s,5.25m。

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

注意:您可能需要從要進行的時間中減去 1 毫秒,因為字母數(shù)字大理石(代表實際的發(fā)射值)在發(fā)射后本身已經(jīng)提前了 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(),或從不發(fā)出或完成的可觀察物

|`: 相當于 `empty()
#`: 相當于 `throwError()

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

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

'-a-^-b--|':在熱觀測下,在 -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助手允許你斷言一個 cold()hot()創(chuàng)建可觀測是訂閱/退訂在正確的時間點。在 subscriptionMarbles對參數(shù) expectObservable允許您的測試,以延遲訂制了更高版本的虛擬時間,和/或即使觀察到被測試尚未完成退訂。

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

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

訂購大理石圖中,最多 應有一個^點,并且最多 應有一個!點。除此之外,該-角色是訂閱大理石圖中唯一允許使用的角色。

例子

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

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

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

'500ms ^ 1s !':在第 500 幀發(fā)生了訂閱,而在第 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');
  });
});

同步斷言

有時,我們需要在可觀察到的流完成斷言狀態(tài)的變化-例如當副作用 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 回調(diào)返回。flush()方法手動觸發(fā)虛擬時間,以便我們在可觀察值完成后測試局部變量。

已知的問題

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

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

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

// 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,您目前也無法斷言零延遲,例如 delay(0)setTimeout(work, 0)。這樣可以安排一個新的“任務”(又稱為“宏任務”),因此它是異步的,但沒有明確的時間間隔。

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

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

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

盡管此時 testScheduler.run(callback)尚未正式棄用外部的 TestScheduler ,但不建議使用它,因為它可能會引起混亂。

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

掃描二維碼

下載編程獅App

公眾號
微信公眾號

編程獅公眾號