RxJSコアチームの元メンバーであるJay Phelps氏は最近RxJSを活用する非同期コードをテストする方法を説明した。このリアクティブプログラミングライブラリは非同期プログラミングでAngularフロントエンドフレームワークで使用される。RxJSは時限シーケンスとライフサイクルイベントを表現するDSLを備えたテストAPIを提供する。
RxJSは、潜在的に無制限の値のシーケンスを生成するメカニズムを囲む Observable
タイプを提供する。値のシーケンスは、プログラムの外部にあるソース (ボタンのクリックなど) から発生する場合もあれば、既存のシーケンスに適用される変換の結果として発生する場合もある。RxJSは、開発者がシーケンス (演算子など) を変換できるようにする追加のAPI、タイプ、およびメソッドを提供する。ソースとシーケンス (Subject
、スケジューラーなど) の間の関係を表現する。
したがって、RxJSを活用する非同期コードは、通常、一連のイベントを Observable
として表現し、それに変換を適用して、ビジネスロジックまたは処理を表現する。結果として得られる observable はさらに変換できる。または、subscribe して、プログラムが observable で囲まれた値に基づいてエフェクト (API呼び出しなど) を実行することもできる。このコンテキストでは、非同期コードのテストは、変換関数が入力 observable を、期待される値のシーケンスを含む出力 observable に取り込むことをテストすることがよくある (たとえば、購入のシーケンスは、更新されたバスケットコンテンツのシーケンスに取り込まれる)。
Phelps氏は、入力シーケンスと出力シーケンスの時間情報が無関係である場合、変換関数のテストは期待される値の配列に対して観測された値の配列をテストするように単純化できることを思い出した:
const input$ = interval(1);
const output$ = input$.pipe(toArray());
output$.subscribe(value => {
expect(value).toEqual([0, 1]);
});
toArray
関数はRxJS APIの一部であり、input$
の値を単一の配列に集約する。toArray
によって返される observable には、その単一の配列が、値のobservable シーケンスの唯一の値として含まれている。interval(1)
は、1秒で区切られた無制限の値のシーケンスだ。したがって、前の例は、制限のないシーケンスがエッジケースを作成する方法を実際に例示している。ここで、output$
observable は、interval
の最後の値が集計を実行するのを待つため、唯一の値を生成することはない。同様のエッジケースの落とし穴の存在は、非同期コードを確実にテストすることが難しいという証拠だ。
以前のテスト手法は、値の配列を比較することで構成されていたため、observable な値のシーケンスから時間情報を削除し、その順序のみを保持していた。シーケンスのタイミングを正確にテストするものである場合、それは不可能だ。
このような場合、RxJSは、時間を仮想化する時限シーケンスとスケジューリングAPIを表現するDSLを提供する。つまり、実際の時間をテスト対象のプログラムの時間から切り離す。DSLは、時間軸上の値を表すマーブル図の形式を取る:
(出典: rxmarbles.com)
前の図は、race
RxJSオペレータを示している。オペレータは一連の observable (ここでは3つが表されている) を取得し、最初に放出されたものを選択する。球は、observable に含まれ、時間軸上に配置されている値を表す。各 observable 表現の最後にあるキャレットは、observable がそれ以上の値を生成しないことを示す (RxJSの用語では、observable は完了したと言われる)。
グラフィカルな表記法は教育および文書化の目的で役立ちますが、RxJSはテストの目的で文字列DSLを使用する。たとえば、文字列 -ab(cd) 9ms e(f|)
は、5つの異なる時点で発行される6つの値 (a、b、c、d、e、f) のシーケンスをエンコードする。一文字は値を表します。ダッシュ記号は時間マーカであり、マークされた時間に observable によって値が生成されないことを意味する。文字列のインデックス t
で見つかった値は、インデックス t-1
の値の後に、observable な固定数の単位 (デフォルトではミリ秒) によって生成されている。|
シンボルは、observable の生成の終了 (完了) をエンコードする。9ms
サブストリングは、前の値の生成を次の値の生成から分離する9ミリ秒の期間を表す。スペース文字は意味をもたない。DSLには追加のルールとエンコーディングがある。完全なマーブルの構文は、。
したがって、文字列によるマーブル図では、時限シーケンスを指定できる。次に、ストリームを変換する関数を、観測されたマーブル図と予想されるマーブル図を比較することでテストできる。RxJSは、TestScheduler
APIと、時限シーケンスをテストするための一連のヘルパメソッドを提供する:
import { TestScheduler } from 'rxjs/testing';
const testScheduler = new TestScheduler((actual, expected) => {
...
expect(actual).deepEqual(expected)
});
testScheduler.run(({expectedObservable}) => {
// Inside here, RxJS time is virtualized
const input$ = interval(1);
const output$ = input$;
const expected = '-ab ';
// `unsub` encodes subscription and unsubscription events
// Here, the test scheduler stops (`!`) processing values after three units of time (`-`) have passed
const unsub = ' ---!';
expectObservable(output$, unsub).toBe(marbles, {
a: 1,
b: 2
})
});
TestScheduler
APIを使用すると、時間を仮想化することにより、非同期のRxJSコードを同期的かつ決定論的にテストできる。ただし、TestScheduler
APIは、タイマ (delay
、debounceTime
など) を使用するコードのテストにのみ使用できる。
前述のAPIと手法は低レベルのままであり、非同期プロセスのより高度なテストを実行するために上に構築するかもしれない。並行プロセスに適用される変換をテストするには、たとえば、プロセスのインターリーブをシミュレートする必要がある。シミュレーションは、期待に一致する出力を作成または維持することが不可能または非現実的な一連のテストケース入力を生成する場合がある。次に、プロパティベースのテストを使用して、生成された出力全体で述語(テスト対象システムのプロパティ)の達成を確認できる。HaskellとQuickCheckの共同設計者であるJohn Hughes氏は、Lambda Daysカンファレンスで、純粋関数のプロパティを特定するための5つの異なる戦略を発表した。ClubCollectの技術リーダであるTomasz Kowal氏は、同じくLambda Daysで、ステートフルプロパティベースのテストの概要を説明した。
RxJSは、observable なシーケンスを使用して非同期およびイベントベースのプログラムを作成するためのライブラリだ。非同期イベントをコレクションとして処理できるように、1つのコアタイプであるObservable、サテライトタイプ (Observer、Scheduler、Subjectsなど)、および演算子 (map、zipなど) を提供する。