この記事ではRxJSの『TestScheduler』について、
- RxJSのTestSchedulerとは
- RxJSのTestSchedulerを用いた基本的なテスト
- マーブル図の構文
- RunHelpersオブジェクト
などを分かりやすく説明するように心掛けています。ご参考になれば幸いです。
RxJSのTestSchedulerとは
TestSchedulerはrxjs/testingモジュールの一部であり、時間に依存するObservableをテストするために用いられます。
TestSchedulerを用いると、時間を仮想化でき、非同期のRxJSコードを同期的にテストすることができるようになります。例えば、5秒後に発生するイベントをテストする際、実際に5秒間待つのではなく、仮想的に5秒間進めることができます。
以下に、TestSchedulerを用いたシンプルなプログラム例を示しています。
it('Observableのテスト', () => {
testScheduler.run((helpers) => {
const { cold, expectObservable } = helpers;
// テスト対象(5秒後に10が流れるObservable)
const source$ = of(10).pipe(delay(5000));
// 5秒後に10が流れるObservableをマーブル文字列を用いて表現
const expected$ = cold('5000ms (a|}', { a: 10 });
// 検証開始
expectObservable(source$).toEqual(expected$);
});
});
後ほど詳しく説明しますが、以下のプログラムについて簡単に説明します。テスト対象として、5秒後に10が流れるObservable(source$
)があります。また、cold
メソッドを用いて、5秒後に10が流れるObservable(expected$
)を作成しています。そして、最後にexpectObservable
を用いて、Observable(source$
)の動作が期待通りか(5秒後に10が流れるか)を検証しています。検証を行う際に、実際に5秒間待つのではなく、’5000ms (a|)’
というマーブル図を文字列で表現したものを用いて、仮想的に5秒間時間進めているのがポイントです。
補足
TestSchedulerでは、マーブル図を文字列で表現したものを用いて、Observableの振る舞いを表現します。各文字が1フレームを表しており、1フレームが10msの場合、-----a
または50ms a
と書けば、50ms後にa
というデータが流れるObservableであるということを意味しています。
RxJSのTestSchedulerを用いた基本的なテスト
以下にRxJSのTestSchedulerを用いた基本的なテストを示しています。以下のプログラムでは、「1→2→3」というデータが流れるObservable(source$
)に対して、map
オペレータを用いて、各データを10倍したものを、Observable(result$
)に代入しています。そして最後に、Observable(result$
)は「10→20→30」というデータが流れているかを検証しています。
import { TestScheduler } from 'rxjs/testing';
import { map } from 'rxjs';
describe('mapオペレータに関するテストスイート', () => {
let testScheduler;
beforeEach(() => {
// TestSchedulerのインスタンスを作成
testScheduler = new TestScheduler((actual, expected) => {
expect(actual).toEqual(expected);
});
});
it('mapオペレータのテスト', () => {
testScheduler.run((helpers) => {
// オブジェクトの分割代入を用いて、Runhelperオブジェクト(helpers)の「coldメソッド」と「expectObservableメソッド」を取得
const { cold, expectObservable } = helpers;
// 「1→2→3」というデータが流れるObservableをマーブル文字列を用いて表現
const source$ = cold('-a-b-c|', { a: 1, b: 2, c: 3 });
// 「10→20→30」というデータが流れるObservableをマーブル文字列を用いて表現
const expected$ = cold('-a-b-c|', { a: 10, b: 20, c: 30 });
// 処理を記述(mapオペレータを用いてsource$の各データを10倍)
const result$ = source$.pipe(map((value) => value * 10));
// 検証開始
expectObservable(result$).toEqual(expected$);
});
});
});
上記のプログラムについて詳しく説明します。
TestSchedulerをインポートする
import { TestScheduler } from 'rxjs/testing';
まず、TestScheduler を使うために、rxjs/testing
からTestSchedulerをインポートします。
describe関数でテストスイートを記述する
describe('mapオペレータに関するテストスイート', () => {
…
});
describe
関数でテストスイート(関連する一連のテストをグループ化したもの)を記述します。describe
関数の中に各単体テストを記述します。
TestSchedulerのインスタンスを作成する
beforeEach(() => {
// TestSchedulerのインスタンスを作成
testScheduler = new TestScheduler((actual, expected) => {
expect(actual).toEqual(expected);
});
});
beforeEach
関数はit
関数やtest
関数で記述された各単体テストが実行される前に実行される関数です。beforeEach
関数の中では、TestScheduler
のインスタンスを作成しています。
TestScheduler
のコンストラクタは、assertDeepEqual
という関数を引数として受け取ります。ここでは、assertDeepEqual
関数として(actual, expected) => expect(actual).toEqual(expected)
を渡しています。
各単体テストをit関数またはtest関数で記述する
it('mapオペレータのテスト', () => {
testScheduler.run((helpers) => {
// オブジェクトの分割代入を用いて、Runhelperオブジェクト(helpers)の「coldメソッド」と「expectObservableメソッド」を取得
const { cold, expectObservable } = helpers;
// テストコードを記述する
});
});
it
関数またはtest
関数を用いて、単体テストを記述します。各単体テストはTestSchedulerのrun
メソッドを用いて記述します。run
メソッドは1つのコールバッグ関数を受け取ります。また、コールバッグ関数はRunhelper
オブジェクトを受け取り、Runhelper
オブジェクトは様々なメソッドがあるので、分割代入で使用するメソッドを取得しています。
Observableを作成する
// 「1→2→3」というデータが流れるObservableをマーブル文字列を用いて表現
const source$ = cold('-a-b-c|', { a: 1, b: 2, c: 3 });
'-a-b-c|'
はマーブル図を文字列で表現しています。各文字が1フレームを表しており、TestSchedulerのrun
メソッドの中では1フレーム1msとなります(1文字目が0ms)。また、a
, b
, c
は流れるデータ、-
は仮想時間の経過(1msの経過)」、|
はObservableの完了を示しています。すなわち、const source$ = cold('-a-b-c|', { a: 1, b: 2, c: 3 });
は以下のObservable(source$
)を作成しています。
- 1ms時に「1」が流れる
- 3ms時に「2」が流れる
- 5ms時に「3」が流れる
- 6ms時にObservableが完了する
また、以下のObservable(expected$)
も作成しています。
// 「10→20→30」というデータが流れるObservableをマーブル文字列を用いて表現
const expected$ = cold('-a-b-c|', { a: 10, b: 20, c: 30 });
const expected$ = cold('-a-b-c|', { a: 10, b: 20, c: 30 });
は以下のObservable(expected$
)を作成しています。
- 1ms時に「10」が流れる
- 3ms時に「20」が流れる
- 5ms時に「30」が流れる
- 6ms時にObservableが完了する
処理を記述する
// 処理を記述(mapオペレータを用いてsource$の各データを10倍)
const result$ = source$.pipe(map((value) => value * 10));
上記の箇所では、Observable(source$
)の各データを10倍し、その結果をObservable(result$
)に代入しています。
検証をする
// 検証開始
expectObservable(result$).toEqual(expected$);
TestSchedulerのexpectObservable
メソッドを使用して、テスト対象のObservable(result$
)が流すデータが期待結果であるObservable(expected$
)と等しいかどうかを検証しています。この行が実行されると、テスト対象のObservable(result$
)が流すデータとそのタイミングを取得し、それを期待結果であるObservable(expected$
)と比較しています。
では次に、マーブル図の構文とRunhelpersオブジェクトについて、もう少し詳しく説明します。
マーブル図の構文
マーブル図の基本的な構文を以下に示します。
- ‘ ‘(空白)
- 空白文字は無視されます。複数のマーブル図を整列させる際に使用します。
- ‘-’(ダッシュ)
- 経過する時間の1フレームを表しています。通常、1フレームは10msですが、TestSchedulerの
run
メソッドの中では1フレーム1msとなります。
- 経過する時間の1フレームを表しています。通常、1フレームは10msですが、TestSchedulerの
- [0-9]+[ms|s|m]
- 特定の時間だけ仮想時間を進めることができます。1フレームが1msの場合、
-----a
と5ms a
は同じ意味になります。
- 特定の時間だけ仮想時間を進めることができます。1フレームが1msの場合、
- [a-z0-9](任意の英数字)
- Observableのデータを表しています。
- ‘|’(パイプ)
- Observableが完了した(全てのデータの放出が終わり、これ以上データを放出しない)ことを表しています。
- ‘#’
- Observableがエラーを放出したことを表しています。
- ‘()’
- グループ化のための括弧です。括弧内の全てのイベントが同じ時間フレームで発生していることを表しています。
- ‘^’
^
の箇所がゼロフレームになります。Hot Observableのサブスクリプションの開始位置を示すのに使用します。
RunHelpersオブジェクト
TestSchedulerの中で最も使うメソッドはrun
メソッドです。run
メソッドはコールバック関数を引数として受け取ります。このコールバック関数内での処理は仮想時間内で行われます。さらに、コールバック関数は引数としてRunHelpers
オブジェクトを受け取ります。これはテストを補助するためのメソッドを持つオブジェクトで、主に以下のメソッドを含んでいます。
- cold
- サブスクリプション時にデータを流すCold Observableを作成します。マーブル図を用いて、Observableの挙動を定義します。
- hot
- Hot Observableを作成します。マーブル図を用いて、Observableの挙動を定義します。Hot ObservableはCold Observableと異なり、サブスクリプション前からデータを流します。
- flush
- 仮想時間を開始するメソッドです。
run
メソッドはコールバッグ関数が返された時に自動的に仮想時間が開始されるため、flush
メソッドはあまり使用されません。
- 仮想時間を開始するメソッドです。
- time
- 「マーブル図(文字列)」を「フレーム数を示す数値」に変換する際に使用します。
- expectObservable
- Observableの挙動をテストする際に使用します。
- expectSubscriptions
- Observableの購読開始のタイミングと購読終了のタイミングをテストする際に使用します。マーブル図(文字列)を引数に受け取ります。
次に、RunHelpers
オブジェクトのhot
, time
, expectSubscriptions
を用いたテストのプログラム例についてこれから説明します。
RxJSのTestSchedulerを用いたテスト(RunHelpersオブジェクトのhotを使用したプログラム例)
it('Hot ObservableとCold Observableのテスト', () => {
testScheduler.run(helpers => {
const { cold, hot, expectObservable } = helpers;
const hotSource$ = hot(' --a^b-c-|', { a: 1, b: 2, c: 3 });
const coldSource$ = cold(' --a-b-c-|', { a: 1, b: 2, c: 3 });
const hotExpected$ = cold(' -b-c-|', { a: 1, b: 2, c: 3 });
const coldExpected$ = cold('--a-b-c-|', { a: 1, b: 2, c: 3 });
expectObservable(hotSource$).toEqual(hotExpected$);
expectObservable(coldSource$).toEqual(coldExpected$);
});
});
Hot Observableはサブスクリプションが開始される前にすでに値の放出が始まります。^
がサブスクリプションが開始されるフレームを示し、その時点でhot Observableはすでにa
のデータが流れています。従って、hotSource$
によって得られる実際の値はb
とc
のみです。
RxJSのTestSchedulerを用いたテスト(RunHelpersオブジェクトのtimeを使用したプログラム例)
it('timeを用いたテスト', () => {
testScheduler.run(helpers => {
const { cold, time, expectObservable } = helpers;
const duration = time('-----|'); // 5フレーム
const source$ = of(10).pipe(delay(duration));
const expected$ = cold('5ms (a|}', { a: 10 });
expectObservable(source$).toEqual(expected$);
});
});
上記のプログラムでは、time('-----|')
により、マーブル図の'-----|'
を数値に変換しています。ここでの'-----|'
は5フレームを意味し、time
メソッドによりこれがミリ秒単位の数値に変換されてduration
に格納されます。const duration = time('-----|');
はTestSchedulerのrun
メソッド内で行われているため、1フレームは1msとなり、duration
は5msとなります。
RxJSのTestSchedulerを用いたテスト(RunHelpersオブジェクトのexpectSubscriptionsを使用したプログラム例)
it('expectSubscriptionsを用いたテスト', () => {
testScheduler.run((helpers) => {
const { cold, expectObservable, expectSubscriptions } = helpers;
// 「1→2→3」というデータが流れるObservableをマーブル文字列を用いて表現
const source$ = cold('-a-b-c|', { a: 1, b: 2, c: 3 });
const expectedSubs = '^-----!'
// 「1→2→3」というデータが流れるObservableをマーブル文字列を用いて表現
const expected$ = cold('-a-b-c|', { a: 1, b: 2, c: 3 });
// この行がないとサブスクリプションが実行されないため、記述
expectObservable(source$).toEqual(expected$);
// source$.subscriptions(実際の購読のタイミング)とexpectedSubs(期待する購読のタイミング)が一致することを検証
expectSubscriptions(source$.subscriptions).toBe(expectedSubs);
});
});
上記のプログラムでは、sourceSubs
で定義された期待される購読のタイミング('^-----!'
)を、source$.subscriptions
と比較しています。^
は購読開始を示し、!
は購読終了を示します。上記のプログラムでは、source$
が購読開始から終了までに予定通り動作しているかを確認しています。
本記事のまとめ
この記事ではRxJSの『TestScheduler』について、以下の内容を説明しました。
- RxJSのTestSchedulerとは
- RxJSのTestSchedulerを用いた基本的なテスト
- マーブル図の構文
- RunHelpersオブジェクト
お読み頂きありがとうございました。