RxJSのTestSchedulerとは?「使い方」などを分かりやすく解説!

この記事では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となります。
  • [0-9]+[ms|s|m]
    • 特定の時間だけ仮想時間を進めることができます。1フレームが1msの場合、-----a5ms aは同じ意味になります。
  • [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$によって得られる実際の値はbcのみです。

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オブジェクト

お読み頂きありがとうございました。