Posted at

RxJS Marble Test 入門

More than 1 year has passed since last update.

When

2016/12/08

At

Meguro.es #7 @ Livesense



今回の(出典)GitHubリポジトリ

ovrmrw/rxjs-marble-tests

===

話す内容


  • RxJS 5のObservableをどうやってテストするか。



参考になるサイト



自己紹介

ちきさん

(Tomohiro Noguchi)

Twitter/GitHub/Qiita: @ovrmrw

ただのSIer。

Angular Japan User Group (ng-japan)スタッフ。

3a2512bb-aa72-4515-af42-1f1721252f39.jpg



アカウントIDの由来


  1. the day after tomorrow

  2. overmorrow(俗語)

  3. 略して

  4. ovrmrw

  5. 「先を見据えて」よりさらに先を、みたいな感じです。

(よく聞かれるので:innocent:)



:tada: RxJS 5はとうとう来週stableがリリースされます! :tada:


(ここから本編)


あるところにこういうObservableがありました。

  Observable.of(1, 2, 3)

.map(value => value * 10)
.filter(value => value > 10)
.do(value => console.log(value));

出力はこうなります。

20

30

mapによって値が10倍されて

filterによって10より大きい値だけが抽出されました。



これが本当にそうなるかをテストします。


先程のObservableをこのように関数でラップします。

Interface: (observable) => observable

function mapFilterTest(observable: Observable<number>): Observable<number> {

return observable
.map(value => value * 10)
.filter(value => value > 10);
}

テストしたいObservableをこのように書き直すのがRxJSのテストの第一歩です。



Observableをテストするために必要なもの


  • source$

  • expected, values

  • actual$

  • expectObservable, flush



source$

const source$: Observable<number> = cold<number>('-a-b-c', { a: 1, b: 2, c: 3 });

coldはObservableを生成する関数です。他にSubjectを生成するhotがあります。

'-a-b-c'がMarbleです。一文字目が0msで、その後一文字分が10msの時間の流れを持ちます。

上記の例で言えばこのようなストリームを表します。


  • 10ms時に値 1 が流れてきた。

  • 30ms時に値 2 が流れてきた。

  • 50ms時に値 3 が流れてきた。



expected, values

const expected = '---B-C';

const values = { A: 10, B: 20, C: 30 };

テスト用語で言うところの Expected を書きます。

expectedの記号にvaluesの値を当てはめていくと考えてください。

この例ではこのような結果を期待しています。


  • 10ms時には何も流れてこないだろう。

  • 30ms時には値 20 が流れてくるだろう。

  • 50ms時には値 30 が流れてくるだろう。



actual$

const actual$: Observable<number> = mapFilterTest(source$);


(mapFilterTest再掲)

function mapFilterTest(observable: Observable<number>): Observable<number> {

return observable
.map(value => value * 10)
.filter(value => value > 10);
}


(source$再掲)

const source$: Observable<number> = cold<number>('-a-b-c', { a: 1, b: 2, c: 3 });


テスト用語で言うところの Actual を書きます。

この結果が先程の Expected と一致するかどうかをテストしたいわけですね。



expectObservable, flush

ts.expectObservable(actual$).toBe(expected, values);

ts.flush();

説明は後回しになりますが、tsはRxJSのTestSchedulerクラスのインスタンスです。

Observableであるactual$が、expectedvaluesの組み合わせと一致することを期待しています。

flushで定義済みのストリームを流します。"Run"みたいな意味合いです。


今まで説明してきたことをitで書くとこんな感じになります。

  it('should return correct observable', () => {

const source$ = cold<number>('-a-b-c', { a: 1, b: 2, c: 3 });
const expected = '---B-C';
const values = { A: 10, B: 20, C: 30 };
const actual$ = mapFilterTest(source$);
ts.expectObservable(actual$).toBe(expected, values);
ts.flush();
});


テストの全容はこのようになります。TestSchedulerクラスを使うのが肝です。

import { Observable, Subject, TestScheduler } from 'rxjs/Rx';

import { assertDeepEqual } from '../testing/helper';

describe('TEST: RxJS Marble Test', () => {
let ts: TestScheduler;
let hot: typeof TestScheduler.prototype.createHotObservable;
let cold: typeof TestScheduler.prototype.createColdObservable;

beforeEach(() => {
ts = new TestScheduler(assertDeepEqual);
hot = ts.createHotObservable.bind(ts);
cold = ts.createColdObservable.bind(ts);
});

it('should return correct observable', () => {
const source$ = cold<number>('-a-b-c', { a: 1, b: 2, c: 3 });
const expected = '-A-B-C';
const values = { A: 10, B: 20, C: 30 };
const actual$ = mapFilterTest(source$);
ts.expectObservable(actual$).toBe(expected, values);
ts.flush();
});
}



Test to fail

  it('should return correct observable', () => {

const source$ = cold<number>('-a-b-c', { a: 1, b: 2, c: 3 });
const expected = '-A-B-C'; // 元は'---B-C'
const values = { A: 10, B: 20, C: 30 };
const actual$ = mapFilterTest(source$);
ts.expectObservable(actual$).toBe(expected, values);
ts.flush();
});

わざとテストを落とすためにexpectedを変更してみました。これにより


  • 10ms時には値 10 が流れてくるだろう。

  • 30ms時には値 20 が流れてくるだろう。

  • 50ms時には値 30 が流れてくるだろう。

という結果を期待することになりますが、実際には10ms時には何も流れてきません。


ActualとExpectedを全てコンソール出力すると結果はこうなります。一致していないことがわかりますね。

========== ACTUAL ==========

[
{
"frame": 30,
"notification": {
"kind": "N",
"value": 20,
"hasValue": true
}
},
{
"frame": 50,
"notification": {
"kind": "N",
"value": 30,
"hasValue": true
}
}
]
========== EXPECTED ==========
[
{
"frame": 10,
"notification": {
"kind": "N",
"value": 10,
"hasValue": true
}
},
{
"frame": 30,
"notification": {
"kind": "N",
"value": 20,
"hasValue": true
}
},
{
"frame": 50,
"notification": {
"kind": "N",
"value": 30,
"hasValue": true
}
}
]


ちなみにこのような関数を自作すると、前ページのような出力結果を得られます。

import * as lodash from 'lodash';

import * as assert from 'assert';

export function assertDeepEqual(actual: any, exptected: any): void {
let messages: string[] = ['\n'];
if (!lodash.isEqual(actual, exptected)) {
messages.push('='.repeat(10) + ' ACTUAL ' + '='.repeat(10));
messages.push(JSON.stringify(actual, null, 2));
messages.push('='.repeat(10) + ' EXPECTED ' + '='.repeat(10));
messages.push(JSON.stringify(exptected, null, 2));
messages.push('\n');
}
assert.deepEqual(actual, exptected, messages.join('\n'));
}

テスト失敗時にactualexpectedの配列をJSON.stringifyして出力しています。



Marble Testを書いてみると、自分が思ってた挙動と違うときがあったりするので救われる:wink:


(今日気付いたこと)


  • Marble Testの中に非同期(Promise等)が混入すると期待通りのテスト結果は得られません。おそらくMarble Testは非同期に非対応です。(自信は無い)



Thanks!